Black format code
This commit is contained in:
parent
34deaf8849
commit
d732a7e670
1368 changed files with 69529 additions and 53340 deletions
|
|
@ -25,6 +25,7 @@ def attempt_use_uvloop() -> None:
|
|||
|
||||
try:
|
||||
import uvloop
|
||||
|
||||
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
|
||||
except ImportError:
|
||||
pass
|
||||
|
|
@ -33,28 +34,40 @@ def attempt_use_uvloop() -> None:
|
|||
def validate_python() -> None:
|
||||
"""Validate that the right Python version is running."""
|
||||
if sys.version_info[:3] < REQUIRED_PYTHON_VER:
|
||||
print("Home Assistant requires at least Python {}.{}.{}".format(
|
||||
*REQUIRED_PYTHON_VER))
|
||||
print(
|
||||
"Home Assistant requires at least Python {}.{}.{}".format(
|
||||
*REQUIRED_PYTHON_VER
|
||||
)
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def ensure_config_path(config_dir: str) -> None:
|
||||
"""Validate the configuration directory."""
|
||||
import homeassistant.config as config_util
|
||||
lib_dir = os.path.join(config_dir, 'deps')
|
||||
|
||||
lib_dir = os.path.join(config_dir, "deps")
|
||||
|
||||
# Test if configuration directory exists
|
||||
if not os.path.isdir(config_dir):
|
||||
if config_dir != config_util.get_default_config_dir():
|
||||
print(('Fatal Error: Specified configuration directory does '
|
||||
'not exist {} ').format(config_dir))
|
||||
print(
|
||||
(
|
||||
"Fatal Error: Specified configuration directory does "
|
||||
"not exist {} "
|
||||
).format(config_dir)
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
os.mkdir(config_dir)
|
||||
except OSError:
|
||||
print(('Fatal Error: Unable to create default configuration '
|
||||
'directory {} ').format(config_dir))
|
||||
print(
|
||||
(
|
||||
"Fatal Error: Unable to create default configuration "
|
||||
"directory {} "
|
||||
).format(config_dir)
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
# Test if library directory exists
|
||||
|
|
@ -62,18 +75,22 @@ def ensure_config_path(config_dir: str) -> None:
|
|||
try:
|
||||
os.mkdir(lib_dir)
|
||||
except OSError:
|
||||
print(('Fatal Error: Unable to create library '
|
||||
'directory {} ').format(lib_dir))
|
||||
print(
|
||||
("Fatal Error: Unable to create library " "directory {} ").format(
|
||||
lib_dir
|
||||
)
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def ensure_config_file(config_dir: str) -> str:
|
||||
"""Ensure configuration file exists."""
|
||||
import homeassistant.config as config_util
|
||||
|
||||
config_path = config_util.ensure_config_exists(config_dir)
|
||||
|
||||
if config_path is None:
|
||||
print('Error getting configuration path')
|
||||
print("Error getting configuration path")
|
||||
sys.exit(1)
|
||||
|
||||
return config_path
|
||||
|
|
@ -82,71 +99,72 @@ def ensure_config_file(config_dir: str) -> str:
|
|||
def get_arguments() -> argparse.Namespace:
|
||||
"""Get parsed passed in arguments."""
|
||||
import homeassistant.config as config_util
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Home Assistant: Observe, Control, Automate.")
|
||||
parser.add_argument('--version', action='version', version=__version__)
|
||||
description="Home Assistant: Observe, Control, Automate."
|
||||
)
|
||||
parser.add_argument("--version", action="version", version=__version__)
|
||||
parser.add_argument(
|
||||
'-c', '--config',
|
||||
metavar='path_to_config_dir',
|
||||
"-c",
|
||||
"--config",
|
||||
metavar="path_to_config_dir",
|
||||
default=config_util.get_default_config_dir(),
|
||||
help="Directory that contains the Home Assistant configuration")
|
||||
help="Directory that contains the Home Assistant configuration",
|
||||
)
|
||||
parser.add_argument(
|
||||
'--demo-mode',
|
||||
action='store_true',
|
||||
help='Start Home Assistant in demo mode')
|
||||
"--demo-mode", action="store_true", help="Start Home Assistant in demo mode"
|
||||
)
|
||||
parser.add_argument(
|
||||
'--debug',
|
||||
action='store_true',
|
||||
help='Start Home Assistant in debug mode')
|
||||
"--debug", action="store_true", help="Start Home Assistant in debug mode"
|
||||
)
|
||||
parser.add_argument(
|
||||
'--open-ui',
|
||||
action='store_true',
|
||||
help='Open the webinterface in a browser')
|
||||
"--open-ui", action="store_true", help="Open the webinterface in a browser"
|
||||
)
|
||||
parser.add_argument(
|
||||
'--skip-pip',
|
||||
action='store_true',
|
||||
help='Skips pip install of required packages on startup')
|
||||
"--skip-pip",
|
||||
action="store_true",
|
||||
help="Skips pip install of required packages on startup",
|
||||
)
|
||||
parser.add_argument(
|
||||
'-v', '--verbose',
|
||||
action='store_true',
|
||||
help="Enable verbose logging to file.")
|
||||
"-v", "--verbose", action="store_true", help="Enable verbose logging to file."
|
||||
)
|
||||
parser.add_argument(
|
||||
'--pid-file',
|
||||
metavar='path_to_pid_file',
|
||||
"--pid-file",
|
||||
metavar="path_to_pid_file",
|
||||
default=None,
|
||||
help='Path to PID file useful for running as daemon')
|
||||
help="Path to PID file useful for running as daemon",
|
||||
)
|
||||
parser.add_argument(
|
||||
'--log-rotate-days',
|
||||
"--log-rotate-days",
|
||||
type=int,
|
||||
default=None,
|
||||
help='Enables daily log rotation and keeps up to the specified days')
|
||||
help="Enables daily log rotation and keeps up to the specified days",
|
||||
)
|
||||
parser.add_argument(
|
||||
'--log-file',
|
||||
"--log-file",
|
||||
type=str,
|
||||
default=None,
|
||||
help='Log file to write to. If not set, CONFIG/home-assistant.log '
|
||||
'is used')
|
||||
help="Log file to write to. If not set, CONFIG/home-assistant.log " "is used",
|
||||
)
|
||||
parser.add_argument(
|
||||
'--log-no-color',
|
||||
action='store_true',
|
||||
help="Disable color logs")
|
||||
"--log-no-color", action="store_true", help="Disable color logs"
|
||||
)
|
||||
parser.add_argument(
|
||||
'--runner',
|
||||
action='store_true',
|
||||
help='On restart exit with code {}'.format(RESTART_EXIT_CODE))
|
||||
"--runner",
|
||||
action="store_true",
|
||||
help="On restart exit with code {}".format(RESTART_EXIT_CODE),
|
||||
)
|
||||
parser.add_argument(
|
||||
'--script',
|
||||
nargs=argparse.REMAINDER,
|
||||
help='Run one of the embedded scripts')
|
||||
"--script", nargs=argparse.REMAINDER, help="Run one of the embedded scripts"
|
||||
)
|
||||
if os.name == "posix":
|
||||
parser.add_argument(
|
||||
'--daemon',
|
||||
action='store_true',
|
||||
help='Run Home Assistant as daemon')
|
||||
"--daemon", action="store_true", help="Run Home Assistant as daemon"
|
||||
)
|
||||
|
||||
arguments = parser.parse_args()
|
||||
if os.name != "posix" or arguments.debug or arguments.runner:
|
||||
setattr(arguments, 'daemon', False)
|
||||
setattr(arguments, "daemon", False)
|
||||
|
||||
return arguments
|
||||
|
||||
|
|
@ -167,8 +185,8 @@ def daemonize() -> None:
|
|||
sys.exit(0)
|
||||
|
||||
# redirect standard file descriptors to devnull
|
||||
infd = open(os.devnull, 'r')
|
||||
outfd = open(os.devnull, 'a+')
|
||||
infd = open(os.devnull, "r")
|
||||
outfd = open(os.devnull, "a+")
|
||||
sys.stdout.flush()
|
||||
sys.stderr.flush()
|
||||
os.dup2(infd.fileno(), sys.stdin.fileno())
|
||||
|
|
@ -180,7 +198,7 @@ def check_pid(pid_file: str) -> None:
|
|||
"""Check that Home Assistant is not already running."""
|
||||
# Check pid file
|
||||
try:
|
||||
with open(pid_file, 'r') as file:
|
||||
with open(pid_file, "r") as file:
|
||||
pid = int(file.readline())
|
||||
except IOError:
|
||||
# PID File does not exist
|
||||
|
|
@ -195,7 +213,7 @@ def check_pid(pid_file: str) -> None:
|
|||
except OSError:
|
||||
# PID does not exist
|
||||
return
|
||||
print('Fatal Error: HomeAssistant is already running.')
|
||||
print("Fatal Error: HomeAssistant is already running.")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
|
|
@ -203,10 +221,10 @@ def write_pid(pid_file: str) -> None:
|
|||
"""Create a PID File."""
|
||||
pid = os.getpid()
|
||||
try:
|
||||
with open(pid_file, 'w') as file:
|
||||
with open(pid_file, "w") as file:
|
||||
file.write(str(pid))
|
||||
except IOError:
|
||||
print('Fatal Error: Unable to write pid file {}'.format(pid_file))
|
||||
print("Fatal Error: Unable to write pid file {}".format(pid_file))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
|
|
@ -230,23 +248,21 @@ def closefds_osx(min_fd: int, max_fd: int) -> None:
|
|||
|
||||
def cmdline() -> List[str]:
|
||||
"""Collect path and arguments to re-execute the current hass instance."""
|
||||
if os.path.basename(sys.argv[0]) == '__main__.py':
|
||||
if os.path.basename(sys.argv[0]) == "__main__.py":
|
||||
modulepath = os.path.dirname(sys.argv[0])
|
||||
os.environ['PYTHONPATH'] = os.path.dirname(modulepath)
|
||||
return [sys.executable] + [arg for arg in sys.argv if
|
||||
arg != '--daemon']
|
||||
os.environ["PYTHONPATH"] = os.path.dirname(modulepath)
|
||||
return [sys.executable] + [arg for arg in sys.argv if arg != "--daemon"]
|
||||
|
||||
return [arg for arg in sys.argv if arg != '--daemon']
|
||||
return [arg for arg in sys.argv if arg != "--daemon"]
|
||||
|
||||
|
||||
def setup_and_run_hass(config_dir: str,
|
||||
args: argparse.Namespace) -> int:
|
||||
def setup_and_run_hass(config_dir: str, args: argparse.Namespace) -> int:
|
||||
"""Set up HASS and run."""
|
||||
from homeassistant import bootstrap
|
||||
|
||||
# Run a simple daemon runner process on Windows to handle restarts
|
||||
if os.name == 'nt' and '--runner' not in sys.argv:
|
||||
nt_args = cmdline() + ['--runner']
|
||||
if os.name == "nt" and "--runner" not in sys.argv:
|
||||
nt_args = cmdline() + ["--runner"]
|
||||
while True:
|
||||
try:
|
||||
subprocess.check_call(nt_args)
|
||||
|
|
@ -256,21 +272,27 @@ def setup_and_run_hass(config_dir: str,
|
|||
sys.exit(exc.returncode)
|
||||
|
||||
if args.demo_mode:
|
||||
config = {
|
||||
'frontend': {},
|
||||
'demo': {}
|
||||
} # type: Dict[str, Any]
|
||||
config = {"frontend": {}, "demo": {}} # type: Dict[str, Any]
|
||||
hass = bootstrap.from_config_dict(
|
||||
config, config_dir=config_dir, verbose=args.verbose,
|
||||
skip_pip=args.skip_pip, log_rotate_days=args.log_rotate_days,
|
||||
log_file=args.log_file, log_no_color=args.log_no_color)
|
||||
config,
|
||||
config_dir=config_dir,
|
||||
verbose=args.verbose,
|
||||
skip_pip=args.skip_pip,
|
||||
log_rotate_days=args.log_rotate_days,
|
||||
log_file=args.log_file,
|
||||
log_no_color=args.log_no_color,
|
||||
)
|
||||
else:
|
||||
config_file = ensure_config_file(config_dir)
|
||||
print('Config directory:', config_dir)
|
||||
print("Config directory:", config_dir)
|
||||
hass = bootstrap.from_config_file(
|
||||
config_file, verbose=args.verbose, skip_pip=args.skip_pip,
|
||||
log_rotate_days=args.log_rotate_days, log_file=args.log_file,
|
||||
log_no_color=args.log_no_color)
|
||||
config_file,
|
||||
verbose=args.verbose,
|
||||
skip_pip=args.skip_pip,
|
||||
log_rotate_days=args.log_rotate_days,
|
||||
log_file=args.log_file,
|
||||
log_no_color=args.log_no_color,
|
||||
)
|
||||
|
||||
if hass is None:
|
||||
return -1
|
||||
|
|
@ -283,12 +305,14 @@ def setup_and_run_hass(config_dir: str,
|
|||
"""Open the web interface in a browser."""
|
||||
if hass.config.api is not None: # type: ignore
|
||||
import webbrowser
|
||||
|
||||
webbrowser.open(hass.config.api.base_url) # type: ignore
|
||||
|
||||
run_callback_threadsafe(
|
||||
hass.loop,
|
||||
hass.bus.async_listen_once,
|
||||
EVENT_HOMEASSISTANT_START, open_browser
|
||||
EVENT_HOMEASSISTANT_START,
|
||||
open_browser,
|
||||
)
|
||||
|
||||
return hass.start()
|
||||
|
|
@ -298,17 +322,17 @@ def try_to_restart() -> None:
|
|||
"""Attempt to clean up state and start a new Home Assistant instance."""
|
||||
# Things should be mostly shut down already at this point, now just try
|
||||
# to clean up things that may have been left behind.
|
||||
sys.stderr.write('Home Assistant attempting to restart.\n')
|
||||
sys.stderr.write("Home Assistant attempting to restart.\n")
|
||||
|
||||
# Count remaining threads, ideally there should only be one non-daemonized
|
||||
# thread left (which is us). Nothing we really do with it, but it might be
|
||||
# useful when debugging shutdown/restart issues.
|
||||
try:
|
||||
nthreads = sum(thread.is_alive() and not thread.daemon
|
||||
for thread in threading.enumerate())
|
||||
nthreads = sum(
|
||||
thread.is_alive() and not thread.daemon for thread in threading.enumerate()
|
||||
)
|
||||
if nthreads > 1:
|
||||
sys.stderr.write(
|
||||
"Found {} non-daemonic threads.\n".format(nthreads))
|
||||
sys.stderr.write("Found {} non-daemonic threads.\n".format(nthreads))
|
||||
|
||||
# Somehow we sometimes seem to trigger an assertion in the python threading
|
||||
# module. It seems we find threads that have no associated OS level thread
|
||||
|
|
@ -322,7 +346,7 @@ def try_to_restart() -> None:
|
|||
except ValueError:
|
||||
max_fd = 256
|
||||
|
||||
if platform.system() == 'Darwin':
|
||||
if platform.system() == "Darwin":
|
||||
closefds_osx(3, max_fd)
|
||||
else:
|
||||
os.closerange(3, max_fd)
|
||||
|
|
@ -341,7 +365,7 @@ def main() -> int:
|
|||
validate_python()
|
||||
|
||||
monkey_patch_needed = sys.version_info[:3] < (3, 6, 3)
|
||||
if monkey_patch_needed and os.environ.get('HASS_NO_MONKEY') != '1':
|
||||
if monkey_patch_needed and os.environ.get("HASS_NO_MONKEY") != "1":
|
||||
if sys.version_info[:2] >= (3, 6):
|
||||
monkey_patch.disable_c_asyncio()
|
||||
monkey_patch.patch_weakref_tasks()
|
||||
|
|
@ -352,6 +376,7 @@ def main() -> int:
|
|||
|
||||
if args.script is not None:
|
||||
from homeassistant import scripts
|
||||
|
||||
return scripts.run(args.script)
|
||||
|
||||
config_dir = os.path.join(os.getcwd(), args.config)
|
||||
|
|
|
|||
|
|
@ -25,7 +25,8 @@ _ProviderDict = Dict[_ProviderKey, AuthProvider]
|
|||
async def auth_manager_from_config(
|
||||
hass: HomeAssistant,
|
||||
provider_configs: List[Dict[str, Any]],
|
||||
module_configs: List[Dict[str, Any]]) -> 'AuthManager':
|
||||
module_configs: List[Dict[str, Any]],
|
||||
) -> "AuthManager":
|
||||
"""Initialize an auth manager from config.
|
||||
|
||||
CORE_CONFIG_SCHEMA will make sure do duplicated auth providers or
|
||||
|
|
@ -34,8 +35,11 @@ async def auth_manager_from_config(
|
|||
store = auth_store.AuthStore(hass)
|
||||
if provider_configs:
|
||||
providers = await asyncio.gather(
|
||||
*[auth_provider_from_config(hass, store, config)
|
||||
for config in provider_configs])
|
||||
*[
|
||||
auth_provider_from_config(hass, store, config)
|
||||
for config in provider_configs
|
||||
]
|
||||
)
|
||||
else:
|
||||
providers = ()
|
||||
# So returned auth providers are in same order as config
|
||||
|
|
@ -46,8 +50,8 @@ async def auth_manager_from_config(
|
|||
|
||||
if module_configs:
|
||||
modules = await asyncio.gather(
|
||||
*[auth_mfa_module_from_config(hass, config)
|
||||
for config in module_configs])
|
||||
*[auth_mfa_module_from_config(hass, config) for config in module_configs]
|
||||
)
|
||||
else:
|
||||
modules = ()
|
||||
# So returned auth modules are in same order as config
|
||||
|
|
@ -62,17 +66,21 @@ async def auth_manager_from_config(
|
|||
class AuthManager:
|
||||
"""Manage the authentication for Home Assistant."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, store: auth_store.AuthStore,
|
||||
providers: _ProviderDict, mfa_modules: _MfaModuleDict) \
|
||||
-> None:
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
store: auth_store.AuthStore,
|
||||
providers: _ProviderDict,
|
||||
mfa_modules: _MfaModuleDict,
|
||||
) -> None:
|
||||
"""Initialize the auth manager."""
|
||||
self.hass = hass
|
||||
self._store = store
|
||||
self._providers = providers
|
||||
self._mfa_modules = mfa_modules
|
||||
self.login_flow = data_entry_flow.FlowManager(
|
||||
hass, self._async_create_login_flow,
|
||||
self._async_finish_login_flow)
|
||||
hass, self._async_create_login_flow, self._async_finish_login_flow
|
||||
)
|
||||
|
||||
@property
|
||||
def active(self) -> bool:
|
||||
|
|
@ -87,7 +95,7 @@ class AuthManager:
|
|||
Should be removed when we removed legacy_api_password auth providers.
|
||||
"""
|
||||
for provider_type, _ in self._providers:
|
||||
if provider_type == 'legacy_api_password':
|
||||
if provider_type == "legacy_api_password":
|
||||
return True
|
||||
return False
|
||||
|
||||
|
|
@ -101,8 +109,7 @@ class AuthManager:
|
|||
"""Return a list of available auth modules."""
|
||||
return list(self._mfa_modules.values())
|
||||
|
||||
def get_auth_mfa_module(self, module_id: str) \
|
||||
-> Optional[MultiFactorAuthModule]:
|
||||
def get_auth_mfa_module(self, module_id: str) -> Optional[MultiFactorAuthModule]:
|
||||
"""Return an multi-factor auth module, None if not found."""
|
||||
return self._mfa_modules.get(module_id)
|
||||
|
||||
|
|
@ -115,7 +122,8 @@ class AuthManager:
|
|||
return await self._store.async_get_user(user_id)
|
||||
|
||||
async def async_get_user_by_credentials(
|
||||
self, credentials: models.Credentials) -> Optional[models.User]:
|
||||
self, credentials: models.Credentials
|
||||
) -> Optional[models.User]:
|
||||
"""Get a user by credential, return None if not found."""
|
||||
for user in await self.async_get_users():
|
||||
for creds in user.credentials:
|
||||
|
|
@ -127,49 +135,43 @@ class AuthManager:
|
|||
async def async_create_system_user(self, name: str) -> models.User:
|
||||
"""Create a system user."""
|
||||
return await self._store.async_create_user(
|
||||
name=name,
|
||||
system_generated=True,
|
||||
is_active=True,
|
||||
name=name, system_generated=True, is_active=True
|
||||
)
|
||||
|
||||
async def async_create_user(self, name: str) -> models.User:
|
||||
"""Create a user."""
|
||||
kwargs = {
|
||||
'name': name,
|
||||
'is_active': True,
|
||||
} # type: Dict[str, Any]
|
||||
kwargs = {"name": name, "is_active": True} # type: Dict[str, Any]
|
||||
|
||||
if await self._user_should_be_owner():
|
||||
kwargs['is_owner'] = True
|
||||
kwargs["is_owner"] = True
|
||||
|
||||
return await self._store.async_create_user(**kwargs)
|
||||
|
||||
async def async_get_or_create_user(self, credentials: models.Credentials) \
|
||||
-> models.User:
|
||||
async def async_get_or_create_user(
|
||||
self, credentials: models.Credentials
|
||||
) -> models.User:
|
||||
"""Get or create a user."""
|
||||
if not credentials.is_new:
|
||||
user = await self.async_get_user_by_credentials(credentials)
|
||||
if user is None:
|
||||
raise ValueError('Unable to find the user.')
|
||||
raise ValueError("Unable to find the user.")
|
||||
else:
|
||||
return user
|
||||
|
||||
auth_provider = self._async_get_auth_provider(credentials)
|
||||
|
||||
if auth_provider is None:
|
||||
raise RuntimeError('Credential with unknown provider encountered')
|
||||
raise RuntimeError("Credential with unknown provider encountered")
|
||||
|
||||
info = await auth_provider.async_user_meta_for_credentials(
|
||||
credentials)
|
||||
info = await auth_provider.async_user_meta_for_credentials(credentials)
|
||||
|
||||
return await self._store.async_create_user(
|
||||
credentials=credentials,
|
||||
name=info.name,
|
||||
is_active=info.is_active,
|
||||
credentials=credentials, name=info.name, is_active=info.is_active
|
||||
)
|
||||
|
||||
async def async_link_user(self, user: models.User,
|
||||
credentials: models.Credentials) -> None:
|
||||
async def async_link_user(
|
||||
self, user: models.User, credentials: models.Credentials
|
||||
) -> None:
|
||||
"""Link credentials to an existing user."""
|
||||
await self._store.async_link_user(user, credentials)
|
||||
|
||||
|
|
@ -192,47 +194,50 @@ class AuthManager:
|
|||
async def async_deactivate_user(self, user: models.User) -> None:
|
||||
"""Deactivate a user."""
|
||||
if user.is_owner:
|
||||
raise ValueError('Unable to deactive the owner')
|
||||
raise ValueError("Unable to deactive the owner")
|
||||
await self._store.async_deactivate_user(user)
|
||||
|
||||
async def async_remove_credentials(
|
||||
self, credentials: models.Credentials) -> None:
|
||||
async def async_remove_credentials(self, credentials: models.Credentials) -> None:
|
||||
"""Remove credentials."""
|
||||
provider = self._async_get_auth_provider(credentials)
|
||||
|
||||
if (provider is not None and
|
||||
hasattr(provider, 'async_will_remove_credentials')):
|
||||
if provider is not None and hasattr(provider, "async_will_remove_credentials"):
|
||||
# https://github.com/python/mypy/issues/1424
|
||||
await provider.async_will_remove_credentials( # type: ignore
|
||||
credentials)
|
||||
await provider.async_will_remove_credentials(credentials) # type: ignore
|
||||
|
||||
await self._store.async_remove_credentials(credentials)
|
||||
|
||||
async def async_enable_user_mfa(self, user: models.User,
|
||||
mfa_module_id: str, data: Any) -> None:
|
||||
async def async_enable_user_mfa(
|
||||
self, user: models.User, mfa_module_id: str, data: Any
|
||||
) -> None:
|
||||
"""Enable a multi-factor auth module for user."""
|
||||
if user.system_generated:
|
||||
raise ValueError('System generated users cannot enable '
|
||||
'multi-factor auth module.')
|
||||
raise ValueError(
|
||||
"System generated users cannot enable " "multi-factor auth module."
|
||||
)
|
||||
|
||||
module = self.get_auth_mfa_module(mfa_module_id)
|
||||
if module is None:
|
||||
raise ValueError('Unable find multi-factor auth module: {}'
|
||||
.format(mfa_module_id))
|
||||
raise ValueError(
|
||||
"Unable find multi-factor auth module: {}".format(mfa_module_id)
|
||||
)
|
||||
|
||||
await module.async_setup_user(user.id, data)
|
||||
|
||||
async def async_disable_user_mfa(self, user: models.User,
|
||||
mfa_module_id: str) -> None:
|
||||
async def async_disable_user_mfa(
|
||||
self, user: models.User, mfa_module_id: str
|
||||
) -> None:
|
||||
"""Disable a multi-factor auth module for user."""
|
||||
if user.system_generated:
|
||||
raise ValueError('System generated users cannot disable '
|
||||
'multi-factor auth module.')
|
||||
raise ValueError(
|
||||
"System generated users cannot disable " "multi-factor auth module."
|
||||
)
|
||||
|
||||
module = self.get_auth_mfa_module(mfa_module_id)
|
||||
if module is None:
|
||||
raise ValueError('Unable find multi-factor auth module: {}'
|
||||
.format(mfa_module_id))
|
||||
raise ValueError(
|
||||
"Unable find multi-factor auth module: {}".format(mfa_module_id)
|
||||
)
|
||||
|
||||
await module.async_depose_user(user.id)
|
||||
|
||||
|
|
@ -245,20 +250,23 @@ class AuthManager:
|
|||
return modules
|
||||
|
||||
async def async_create_refresh_token(
|
||||
self, user: models.User, client_id: Optional[str] = None,
|
||||
self,
|
||||
user: models.User,
|
||||
client_id: Optional[str] = None,
|
||||
client_name: Optional[str] = None,
|
||||
client_icon: Optional[str] = None,
|
||||
token_type: Optional[str] = None,
|
||||
access_token_expiration: timedelta = ACCESS_TOKEN_EXPIRATION) \
|
||||
-> models.RefreshToken:
|
||||
access_token_expiration: timedelta = ACCESS_TOKEN_EXPIRATION,
|
||||
) -> models.RefreshToken:
|
||||
"""Create a new refresh token for a user."""
|
||||
if not user.is_active:
|
||||
raise ValueError('User is not active')
|
||||
raise ValueError("User is not active")
|
||||
|
||||
if user.system_generated and client_id is not None:
|
||||
raise ValueError(
|
||||
'System generated users cannot have refresh tokens connected '
|
||||
'to a client.')
|
||||
"System generated users cannot have refresh tokens connected "
|
||||
"to a client."
|
||||
)
|
||||
|
||||
if token_type is None:
|
||||
if user.system_generated:
|
||||
|
|
@ -268,62 +276,77 @@ class AuthManager:
|
|||
|
||||
if user.system_generated != (token_type == models.TOKEN_TYPE_SYSTEM):
|
||||
raise ValueError(
|
||||
'System generated users can only have system type '
|
||||
'refresh tokens')
|
||||
"System generated users can only have system type " "refresh tokens"
|
||||
)
|
||||
|
||||
if token_type == models.TOKEN_TYPE_NORMAL and client_id is None:
|
||||
raise ValueError('Client is required to generate a refresh token.')
|
||||
raise ValueError("Client is required to generate a refresh token.")
|
||||
|
||||
if (token_type == models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN and
|
||||
client_name is None):
|
||||
raise ValueError('Client_name is required for long-lived access '
|
||||
'token')
|
||||
if (
|
||||
token_type == models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN
|
||||
and client_name is None
|
||||
):
|
||||
raise ValueError("Client_name is required for long-lived access " "token")
|
||||
|
||||
if token_type == models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN:
|
||||
for token in user.refresh_tokens.values():
|
||||
if (token.client_name == client_name and token.token_type ==
|
||||
models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN):
|
||||
if (
|
||||
token.client_name == client_name
|
||||
and token.token_type == models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN
|
||||
):
|
||||
# Each client_name can only have one
|
||||
# long_lived_access_token type of refresh token
|
||||
raise ValueError('{} already exists'.format(client_name))
|
||||
raise ValueError("{} already exists".format(client_name))
|
||||
|
||||
return await self._store.async_create_refresh_token(
|
||||
user, client_id, client_name, client_icon,
|
||||
token_type, access_token_expiration)
|
||||
user,
|
||||
client_id,
|
||||
client_name,
|
||||
client_icon,
|
||||
token_type,
|
||||
access_token_expiration,
|
||||
)
|
||||
|
||||
async def async_get_refresh_token(
|
||||
self, token_id: str) -> Optional[models.RefreshToken]:
|
||||
self, token_id: str
|
||||
) -> Optional[models.RefreshToken]:
|
||||
"""Get refresh token by id."""
|
||||
return await self._store.async_get_refresh_token(token_id)
|
||||
|
||||
async def async_get_refresh_token_by_token(
|
||||
self, token: str) -> Optional[models.RefreshToken]:
|
||||
self, token: str
|
||||
) -> Optional[models.RefreshToken]:
|
||||
"""Get refresh token by token."""
|
||||
return await self._store.async_get_refresh_token_by_token(token)
|
||||
|
||||
async def async_remove_refresh_token(self,
|
||||
refresh_token: models.RefreshToken) \
|
||||
-> None:
|
||||
async def async_remove_refresh_token(
|
||||
self, refresh_token: models.RefreshToken
|
||||
) -> None:
|
||||
"""Delete a refresh token."""
|
||||
await self._store.async_remove_refresh_token(refresh_token)
|
||||
|
||||
@callback
|
||||
def async_create_access_token(self,
|
||||
refresh_token: models.RefreshToken,
|
||||
remote_ip: Optional[str] = None) -> str:
|
||||
def async_create_access_token(
|
||||
self, refresh_token: models.RefreshToken, remote_ip: Optional[str] = None
|
||||
) -> str:
|
||||
"""Create a new access token."""
|
||||
self._store.async_log_refresh_token_usage(refresh_token, remote_ip)
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
now = dt_util.utcnow()
|
||||
return jwt.encode({
|
||||
'iss': refresh_token.id,
|
||||
'iat': now,
|
||||
'exp': now + refresh_token.access_token_expiration,
|
||||
}, refresh_token.jwt_key, algorithm='HS256').decode()
|
||||
return jwt.encode(
|
||||
{
|
||||
"iss": refresh_token.id,
|
||||
"iat": now,
|
||||
"exp": now + refresh_token.access_token_expiration,
|
||||
},
|
||||
refresh_token.jwt_key,
|
||||
algorithm="HS256",
|
||||
).decode()
|
||||
|
||||
async def async_validate_access_token(
|
||||
self, token: str) -> Optional[models.RefreshToken]:
|
||||
self, token: str
|
||||
) -> Optional[models.RefreshToken]:
|
||||
"""Return refresh token if an access token is valid."""
|
||||
try:
|
||||
unverif_claims = jwt.decode(token, verify=False)
|
||||
|
|
@ -331,23 +354,18 @@ class AuthManager:
|
|||
return None
|
||||
|
||||
refresh_token = await self.async_get_refresh_token(
|
||||
cast(str, unverif_claims.get('iss')))
|
||||
cast(str, unverif_claims.get("iss"))
|
||||
)
|
||||
|
||||
if refresh_token is None:
|
||||
jwt_key = ''
|
||||
issuer = ''
|
||||
jwt_key = ""
|
||||
issuer = ""
|
||||
else:
|
||||
jwt_key = refresh_token.jwt_key
|
||||
issuer = refresh_token.id
|
||||
|
||||
try:
|
||||
jwt.decode(
|
||||
token,
|
||||
jwt_key,
|
||||
leeway=10,
|
||||
issuer=issuer,
|
||||
algorithms=['HS256']
|
||||
)
|
||||
jwt.decode(token, jwt_key, leeway=10, issuer=issuer, algorithms=["HS256"])
|
||||
except jwt.InvalidTokenError:
|
||||
return None
|
||||
|
||||
|
|
@ -357,31 +375,32 @@ class AuthManager:
|
|||
return refresh_token
|
||||
|
||||
async def _async_create_login_flow(
|
||||
self, handler: _ProviderKey, *, context: Optional[Dict],
|
||||
data: Optional[Any]) -> data_entry_flow.FlowHandler:
|
||||
self, handler: _ProviderKey, *, context: Optional[Dict], data: Optional[Any]
|
||||
) -> data_entry_flow.FlowHandler:
|
||||
"""Create a login flow."""
|
||||
auth_provider = self._providers[handler]
|
||||
|
||||
return await auth_provider.async_login_flow(context)
|
||||
|
||||
async def _async_finish_login_flow(
|
||||
self, flow: LoginFlow, result: Dict[str, Any]) \
|
||||
-> Dict[str, Any]:
|
||||
self, flow: LoginFlow, result: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""Return a user as result of login flow."""
|
||||
if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
|
||||
if result["type"] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
|
||||
return result
|
||||
|
||||
# we got final result
|
||||
if isinstance(result['data'], models.User):
|
||||
result['result'] = result['data']
|
||||
if isinstance(result["data"], models.User):
|
||||
result["result"] = result["data"]
|
||||
return result
|
||||
|
||||
auth_provider = self._providers[result['handler']]
|
||||
auth_provider = self._providers[result["handler"]]
|
||||
credentials = await auth_provider.async_get_or_create_credentials(
|
||||
result['data'])
|
||||
result["data"]
|
||||
)
|
||||
|
||||
if flow.context is not None and flow.context.get('credential_only'):
|
||||
result['result'] = credentials
|
||||
if flow.context is not None and flow.context.get("credential_only"):
|
||||
result["result"] = credentials
|
||||
return result
|
||||
|
||||
# multi-factor module cannot enabled for new credential
|
||||
|
|
@ -396,15 +415,18 @@ class AuthManager:
|
|||
flow.available_mfa_modules = modules
|
||||
return await flow.async_step_select_mfa_module()
|
||||
|
||||
result['result'] = await self.async_get_or_create_user(credentials)
|
||||
result["result"] = await self.async_get_or_create_user(credentials)
|
||||
return result
|
||||
|
||||
@callback
|
||||
def _async_get_auth_provider(
|
||||
self, credentials: models.Credentials) -> Optional[AuthProvider]:
|
||||
self, credentials: models.Credentials
|
||||
) -> Optional[AuthProvider]:
|
||||
"""Get auth provider from a set of credentials."""
|
||||
auth_provider_key = (credentials.auth_provider_type,
|
||||
credentials.auth_provider_id)
|
||||
auth_provider_key = (
|
||||
credentials.auth_provider_type,
|
||||
credentials.auth_provider_id,
|
||||
)
|
||||
return self._providers.get(auth_provider_key)
|
||||
|
||||
async def _user_should_be_owner(self) -> bool:
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ from homeassistant.util import dt as dt_util
|
|||
from . import models
|
||||
|
||||
STORAGE_VERSION = 1
|
||||
STORAGE_KEY = 'auth'
|
||||
STORAGE_KEY = "auth"
|
||||
|
||||
|
||||
class AuthStore:
|
||||
|
|
@ -47,27 +47,28 @@ class AuthStore:
|
|||
return self._users.get(user_id)
|
||||
|
||||
async def async_create_user(
|
||||
self, name: Optional[str], is_owner: Optional[bool] = None,
|
||||
self,
|
||||
name: Optional[str],
|
||||
is_owner: Optional[bool] = None,
|
||||
is_active: Optional[bool] = None,
|
||||
system_generated: Optional[bool] = None,
|
||||
credentials: Optional[models.Credentials] = None) -> models.User:
|
||||
credentials: Optional[models.Credentials] = None,
|
||||
) -> models.User:
|
||||
"""Create a new user."""
|
||||
if self._users is None:
|
||||
await self._async_load()
|
||||
assert self._users is not None
|
||||
|
||||
kwargs = {
|
||||
'name': name
|
||||
} # type: Dict[str, Any]
|
||||
kwargs = {"name": name} # type: Dict[str, Any]
|
||||
|
||||
if is_owner is not None:
|
||||
kwargs['is_owner'] = is_owner
|
||||
kwargs["is_owner"] = is_owner
|
||||
|
||||
if is_active is not None:
|
||||
kwargs['is_active'] = is_active
|
||||
kwargs["is_active"] = is_active
|
||||
|
||||
if system_generated is not None:
|
||||
kwargs['system_generated'] = system_generated
|
||||
kwargs["system_generated"] = system_generated
|
||||
|
||||
new_user = models.User(**kwargs)
|
||||
|
||||
|
|
@ -81,8 +82,9 @@ class AuthStore:
|
|||
await self.async_link_user(new_user, credentials)
|
||||
return new_user
|
||||
|
||||
async def async_link_user(self, user: models.User,
|
||||
credentials: models.Credentials) -> None:
|
||||
async def async_link_user(
|
||||
self, user: models.User, credentials: models.Credentials
|
||||
) -> None:
|
||||
"""Add credentials to an existing user."""
|
||||
user.credentials.append(credentials)
|
||||
self._async_schedule_save()
|
||||
|
|
@ -107,8 +109,7 @@ class AuthStore:
|
|||
user.is_active = False
|
||||
self._async_schedule_save()
|
||||
|
||||
async def async_remove_credentials(
|
||||
self, credentials: models.Credentials) -> None:
|
||||
async def async_remove_credentials(self, credentials: models.Credentials) -> None:
|
||||
"""Remove credentials."""
|
||||
if self._users is None:
|
||||
await self._async_load()
|
||||
|
|
@ -129,23 +130,25 @@ class AuthStore:
|
|||
self._async_schedule_save()
|
||||
|
||||
async def async_create_refresh_token(
|
||||
self, user: models.User, client_id: Optional[str] = None,
|
||||
self,
|
||||
user: models.User,
|
||||
client_id: Optional[str] = None,
|
||||
client_name: Optional[str] = None,
|
||||
client_icon: Optional[str] = None,
|
||||
token_type: str = models.TOKEN_TYPE_NORMAL,
|
||||
access_token_expiration: timedelta = ACCESS_TOKEN_EXPIRATION) \
|
||||
-> models.RefreshToken:
|
||||
access_token_expiration: timedelta = ACCESS_TOKEN_EXPIRATION,
|
||||
) -> models.RefreshToken:
|
||||
"""Create a new token for a user."""
|
||||
kwargs = {
|
||||
'user': user,
|
||||
'client_id': client_id,
|
||||
'token_type': token_type,
|
||||
'access_token_expiration': access_token_expiration
|
||||
"user": user,
|
||||
"client_id": client_id,
|
||||
"token_type": token_type,
|
||||
"access_token_expiration": access_token_expiration,
|
||||
} # type: Dict[str, Any]
|
||||
if client_name:
|
||||
kwargs['client_name'] = client_name
|
||||
kwargs["client_name"] = client_name
|
||||
if client_icon:
|
||||
kwargs['client_icon'] = client_icon
|
||||
kwargs["client_icon"] = client_icon
|
||||
|
||||
refresh_token = models.RefreshToken(**kwargs)
|
||||
user.refresh_tokens[refresh_token.id] = refresh_token
|
||||
|
|
@ -154,7 +157,8 @@ class AuthStore:
|
|||
return refresh_token
|
||||
|
||||
async def async_remove_refresh_token(
|
||||
self, refresh_token: models.RefreshToken) -> None:
|
||||
self, refresh_token: models.RefreshToken
|
||||
) -> None:
|
||||
"""Remove a refresh token."""
|
||||
if self._users is None:
|
||||
await self._async_load()
|
||||
|
|
@ -166,7 +170,8 @@ class AuthStore:
|
|||
break
|
||||
|
||||
async def async_get_refresh_token(
|
||||
self, token_id: str) -> Optional[models.RefreshToken]:
|
||||
self, token_id: str
|
||||
) -> Optional[models.RefreshToken]:
|
||||
"""Get refresh token by id."""
|
||||
if self._users is None:
|
||||
await self._async_load()
|
||||
|
|
@ -180,7 +185,8 @@ class AuthStore:
|
|||
return None
|
||||
|
||||
async def async_get_refresh_token_by_token(
|
||||
self, token: str) -> Optional[models.RefreshToken]:
|
||||
self, token: str
|
||||
) -> Optional[models.RefreshToken]:
|
||||
"""Get refresh token by token."""
|
||||
if self._users is None:
|
||||
await self._async_load()
|
||||
|
|
@ -197,8 +203,8 @@ class AuthStore:
|
|||
|
||||
@callback
|
||||
def async_log_refresh_token_usage(
|
||||
self, refresh_token: models.RefreshToken,
|
||||
remote_ip: Optional[str] = None) -> None:
|
||||
self, refresh_token: models.RefreshToken, remote_ip: Optional[str] = None
|
||||
) -> None:
|
||||
"""Update refresh token last used information."""
|
||||
refresh_token.last_used_at = dt_util.utcnow()
|
||||
refresh_token.last_used_ip = remote_ip
|
||||
|
|
@ -219,61 +225,66 @@ class AuthStore:
|
|||
self._users = users
|
||||
return
|
||||
|
||||
for user_dict in data['users']:
|
||||
users[user_dict['id']] = models.User(**user_dict)
|
||||
for user_dict in data["users"]:
|
||||
users[user_dict["id"]] = models.User(**user_dict)
|
||||
|
||||
for cred_dict in data['credentials']:
|
||||
users[cred_dict['user_id']].credentials.append(models.Credentials(
|
||||
id=cred_dict['id'],
|
||||
for cred_dict in data["credentials"]:
|
||||
users[cred_dict["user_id"]].credentials.append(
|
||||
models.Credentials(
|
||||
id=cred_dict["id"],
|
||||
is_new=False,
|
||||
auth_provider_type=cred_dict['auth_provider_type'],
|
||||
auth_provider_id=cred_dict['auth_provider_id'],
|
||||
data=cred_dict['data'],
|
||||
))
|
||||
auth_provider_type=cred_dict["auth_provider_type"],
|
||||
auth_provider_id=cred_dict["auth_provider_id"],
|
||||
data=cred_dict["data"],
|
||||
)
|
||||
)
|
||||
|
||||
for rt_dict in data['refresh_tokens']:
|
||||
for rt_dict in data["refresh_tokens"]:
|
||||
# Filter out the old keys that don't have jwt_key (pre-0.76)
|
||||
if 'jwt_key' not in rt_dict:
|
||||
if "jwt_key" not in rt_dict:
|
||||
continue
|
||||
|
||||
created_at = dt_util.parse_datetime(rt_dict['created_at'])
|
||||
created_at = dt_util.parse_datetime(rt_dict["created_at"])
|
||||
if created_at is None:
|
||||
getLogger(__name__).error(
|
||||
'Ignoring refresh token %(id)s with invalid created_at '
|
||||
'%(created_at)s for user_id %(user_id)s', rt_dict)
|
||||
"Ignoring refresh token %(id)s with invalid created_at "
|
||||
"%(created_at)s for user_id %(user_id)s",
|
||||
rt_dict,
|
||||
)
|
||||
continue
|
||||
|
||||
token_type = rt_dict.get('token_type')
|
||||
token_type = rt_dict.get("token_type")
|
||||
if token_type is None:
|
||||
if rt_dict['client_id'] is None:
|
||||
if rt_dict["client_id"] is None:
|
||||
token_type = models.TOKEN_TYPE_SYSTEM
|
||||
else:
|
||||
token_type = models.TOKEN_TYPE_NORMAL
|
||||
|
||||
# old refresh_token don't have last_used_at (pre-0.78)
|
||||
last_used_at_str = rt_dict.get('last_used_at')
|
||||
last_used_at_str = rt_dict.get("last_used_at")
|
||||
if last_used_at_str:
|
||||
last_used_at = dt_util.parse_datetime(last_used_at_str)
|
||||
else:
|
||||
last_used_at = None
|
||||
|
||||
token = models.RefreshToken(
|
||||
id=rt_dict['id'],
|
||||
user=users[rt_dict['user_id']],
|
||||
client_id=rt_dict['client_id'],
|
||||
id=rt_dict["id"],
|
||||
user=users[rt_dict["user_id"]],
|
||||
client_id=rt_dict["client_id"],
|
||||
# use dict.get to keep backward compatibility
|
||||
client_name=rt_dict.get('client_name'),
|
||||
client_icon=rt_dict.get('client_icon'),
|
||||
client_name=rt_dict.get("client_name"),
|
||||
client_icon=rt_dict.get("client_icon"),
|
||||
token_type=token_type,
|
||||
created_at=created_at,
|
||||
access_token_expiration=timedelta(
|
||||
seconds=rt_dict['access_token_expiration']),
|
||||
token=rt_dict['token'],
|
||||
jwt_key=rt_dict['jwt_key'],
|
||||
seconds=rt_dict["access_token_expiration"]
|
||||
),
|
||||
token=rt_dict["token"],
|
||||
jwt_key=rt_dict["jwt_key"],
|
||||
last_used_at=last_used_at,
|
||||
last_used_ip=rt_dict.get('last_used_ip'),
|
||||
last_used_ip=rt_dict.get("last_used_ip"),
|
||||
)
|
||||
users[rt_dict['user_id']].refresh_tokens[token.id] = token
|
||||
users[rt_dict["user_id"]].refresh_tokens[token.id] = token
|
||||
|
||||
self._users = users
|
||||
|
||||
|
|
@ -292,22 +303,22 @@ class AuthStore:
|
|||
|
||||
users = [
|
||||
{
|
||||
'id': user.id,
|
||||
'is_owner': user.is_owner,
|
||||
'is_active': user.is_active,
|
||||
'name': user.name,
|
||||
'system_generated': user.system_generated,
|
||||
"id": user.id,
|
||||
"is_owner": user.is_owner,
|
||||
"is_active": user.is_active,
|
||||
"name": user.name,
|
||||
"system_generated": user.system_generated,
|
||||
}
|
||||
for user in self._users.values()
|
||||
]
|
||||
|
||||
credentials = [
|
||||
{
|
||||
'id': credential.id,
|
||||
'user_id': user.id,
|
||||
'auth_provider_type': credential.auth_provider_type,
|
||||
'auth_provider_id': credential.auth_provider_id,
|
||||
'data': credential.data,
|
||||
"id": credential.id,
|
||||
"user_id": user.id,
|
||||
"auth_provider_type": credential.auth_provider_type,
|
||||
"auth_provider_id": credential.auth_provider_id,
|
||||
"data": credential.data,
|
||||
}
|
||||
for user in self._users.values()
|
||||
for credential in user.credentials
|
||||
|
|
@ -315,28 +326,27 @@ class AuthStore:
|
|||
|
||||
refresh_tokens = [
|
||||
{
|
||||
'id': refresh_token.id,
|
||||
'user_id': user.id,
|
||||
'client_id': refresh_token.client_id,
|
||||
'client_name': refresh_token.client_name,
|
||||
'client_icon': refresh_token.client_icon,
|
||||
'token_type': refresh_token.token_type,
|
||||
'created_at': refresh_token.created_at.isoformat(),
|
||||
'access_token_expiration':
|
||||
refresh_token.access_token_expiration.total_seconds(),
|
||||
'token': refresh_token.token,
|
||||
'jwt_key': refresh_token.jwt_key,
|
||||
'last_used_at':
|
||||
refresh_token.last_used_at.isoformat()
|
||||
if refresh_token.last_used_at else None,
|
||||
'last_used_ip': refresh_token.last_used_ip,
|
||||
"id": refresh_token.id,
|
||||
"user_id": user.id,
|
||||
"client_id": refresh_token.client_id,
|
||||
"client_name": refresh_token.client_name,
|
||||
"client_icon": refresh_token.client_icon,
|
||||
"token_type": refresh_token.token_type,
|
||||
"created_at": refresh_token.created_at.isoformat(),
|
||||
"access_token_expiration": refresh_token.access_token_expiration.total_seconds(),
|
||||
"token": refresh_token.token,
|
||||
"jwt_key": refresh_token.jwt_key,
|
||||
"last_used_at": refresh_token.last_used_at.isoformat()
|
||||
if refresh_token.last_used_at
|
||||
else None,
|
||||
"last_used_ip": refresh_token.last_used_ip,
|
||||
}
|
||||
for user in self._users.values()
|
||||
for refresh_token in user.refresh_tokens.values()
|
||||
]
|
||||
|
||||
return {
|
||||
'users': users,
|
||||
'credentials': credentials,
|
||||
'refresh_tokens': refresh_tokens,
|
||||
"users": users,
|
||||
"credentials": credentials,
|
||||
"refresh_tokens": refresh_tokens,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,16 +16,19 @@ from homeassistant.util.decorator import 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,
|
||||
# Specify ID if you have two mfa auth module for same type.
|
||||
vol.Optional(CONF_ID): str,
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
SESSION_EXPIRATION = timedelta(minutes=5)
|
||||
|
||||
DATA_REQS = 'mfa_auth_module_reqs_processed'
|
||||
DATA_REQS = "mfa_auth_module_reqs_processed"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -33,7 +36,7 @@ _LOGGER = logging.getLogger(__name__)
|
|||
class MultiFactorAuthModule:
|
||||
"""Multi-factor Auth Module of validation function."""
|
||||
|
||||
DEFAULT_TITLE = 'Unnamed auth module'
|
||||
DEFAULT_TITLE = "Unnamed auth module"
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: Dict[str, Any]) -> None:
|
||||
"""Initialize an auth module."""
|
||||
|
|
@ -65,7 +68,7 @@ class MultiFactorAuthModule:
|
|||
"""Return a voluptuous schema to define mfa auth module's input."""
|
||||
raise NotImplementedError
|
||||
|
||||
async def async_setup_flow(self, user_id: str) -> 'SetupFlow':
|
||||
async def async_setup_flow(self, user_id: str) -> "SetupFlow":
|
||||
"""Return a data entry flow handler for setup module.
|
||||
|
||||
Mfa module should extend SetupFlow
|
||||
|
|
@ -84,8 +87,7 @@ class MultiFactorAuthModule:
|
|||
"""Return whether user is setup."""
|
||||
raise NotImplementedError
|
||||
|
||||
async def async_validation(
|
||||
self, user_id: str, user_input: Dict[str, Any]) -> bool:
|
||||
async def async_validation(self, user_id: str, user_input: Dict[str, Any]) -> bool:
|
||||
"""Return True if validation passed."""
|
||||
raise NotImplementedError
|
||||
|
||||
|
|
@ -93,17 +95,17 @@ class MultiFactorAuthModule:
|
|||
class SetupFlow(data_entry_flow.FlowHandler):
|
||||
"""Handler for the setup flow."""
|
||||
|
||||
def __init__(self, auth_module: MultiFactorAuthModule,
|
||||
setup_schema: vol.Schema,
|
||||
user_id: str) -> None:
|
||||
def __init__(
|
||||
self, auth_module: MultiFactorAuthModule, setup_schema: vol.Schema, user_id: str
|
||||
) -> None:
|
||||
"""Initialize the setup flow."""
|
||||
self._auth_module = auth_module
|
||||
self._setup_schema = setup_schema
|
||||
self._user_id = user_id
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: Optional[Dict[str, str]] = None) \
|
||||
-> Dict[str, Any]:
|
||||
self, user_input: Optional[Dict[str, str]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Handle the first step of setup flow.
|
||||
|
||||
Return self.async_show_form(step_id='init') if user_input == None.
|
||||
|
|
@ -112,23 +114,19 @@ class SetupFlow(data_entry_flow.FlowHandler):
|
|||
errors = {} # type: Dict[str, str]
|
||||
|
||||
if user_input:
|
||||
result = await self._auth_module.async_setup_user(
|
||||
self._user_id, user_input)
|
||||
result = await self._auth_module.async_setup_user(self._user_id, user_input)
|
||||
return self.async_create_entry(
|
||||
title=self._auth_module.name,
|
||||
data={'result': result}
|
||||
title=self._auth_module.name, data={"result": result}
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id='init',
|
||||
data_schema=self._setup_schema,
|
||||
errors=errors
|
||||
step_id="init", data_schema=self._setup_schema, errors=errors
|
||||
)
|
||||
|
||||
|
||||
async def auth_mfa_module_from_config(
|
||||
hass: HomeAssistant, config: Dict[str, Any]) \
|
||||
-> MultiFactorAuthModule:
|
||||
hass: HomeAssistant, config: Dict[str, Any]
|
||||
) -> MultiFactorAuthModule:
|
||||
"""Initialize an auth module from a config."""
|
||||
module_name = config[CONF_TYPE]
|
||||
module = await _load_mfa_module(hass, module_name)
|
||||
|
|
@ -136,26 +134,29 @@ async def auth_mfa_module_from_config(
|
|||
try:
|
||||
config = module.CONFIG_SCHEMA(config) # type: ignore
|
||||
except vol.Invalid as err:
|
||||
_LOGGER.error('Invalid configuration for multi-factor module %s: %s',
|
||||
module_name, humanize_error(config, err))
|
||||
_LOGGER.error(
|
||||
"Invalid configuration for multi-factor module %s: %s",
|
||||
module_name,
|
||||
humanize_error(config, err),
|
||||
)
|
||||
raise
|
||||
|
||||
return MULTI_FACTOR_AUTH_MODULES[module_name](hass, config) # type: ignore
|
||||
|
||||
|
||||
async def _load_mfa_module(hass: HomeAssistant, module_name: str) \
|
||||
-> types.ModuleType:
|
||||
async def _load_mfa_module(hass: HomeAssistant, module_name: str) -> types.ModuleType:
|
||||
"""Load an mfa auth module."""
|
||||
module_path = 'homeassistant.auth.mfa_modules.{}'.format(module_name)
|
||||
module_path = "homeassistant.auth.mfa_modules.{}".format(module_name)
|
||||
|
||||
try:
|
||||
module = importlib.import_module(module_path)
|
||||
except ImportError as err:
|
||||
_LOGGER.error('Unable to load mfa module %s: %s', module_name, err)
|
||||
raise HomeAssistantError('Unable to load mfa module {}: {}'.format(
|
||||
module_name, err))
|
||||
_LOGGER.error("Unable to load mfa module %s: %s", module_name, err)
|
||||
raise HomeAssistantError(
|
||||
"Unable to load mfa module {}: {}".format(module_name, err)
|
||||
)
|
||||
|
||||
if hass.config.skip_pip or not hasattr(module, 'REQUIREMENTS'):
|
||||
if hass.config.skip_pip or not hasattr(module, "REQUIREMENTS"):
|
||||
return module
|
||||
|
||||
processed = hass.data.get(DATA_REQS)
|
||||
|
|
@ -166,12 +167,13 @@ async def _load_mfa_module(hass: HomeAssistant, module_name: str) \
|
|||
|
||||
# https://github.com/python/mypy/issues/1424
|
||||
req_success = await requirements.async_process_requirements(
|
||||
hass, module_path, module.REQUIREMENTS) # type: ignore
|
||||
hass, module_path, module.REQUIREMENTS
|
||||
) # type: ignore
|
||||
|
||||
if not req_success:
|
||||
raise HomeAssistantError(
|
||||
'Unable to process requirements of mfa module {}'.format(
|
||||
module_name))
|
||||
"Unable to process requirements of mfa module {}".format(module_name)
|
||||
)
|
||||
|
||||
processed.add(module_name)
|
||||
return module
|
||||
|
|
|
|||
|
|
@ -6,39 +6,45 @@ import voluptuous as vol
|
|||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import MultiFactorAuthModule, MULTI_FACTOR_AUTH_MODULES, \
|
||||
MULTI_FACTOR_AUTH_MODULE_SCHEMA, SetupFlow
|
||||
from . import (
|
||||
MultiFactorAuthModule,
|
||||
MULTI_FACTOR_AUTH_MODULES,
|
||||
MULTI_FACTOR_AUTH_MODULE_SCHEMA,
|
||||
SetupFlow,
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = MULTI_FACTOR_AUTH_MODULE_SCHEMA.extend({
|
||||
vol.Required('data'): [vol.Schema({
|
||||
vol.Required('user_id'): str,
|
||||
vol.Required('pin'): str,
|
||||
})]
|
||||
}, extra=vol.PREVENT_EXTRA)
|
||||
CONFIG_SCHEMA = MULTI_FACTOR_AUTH_MODULE_SCHEMA.extend(
|
||||
{
|
||||
vol.Required("data"): [
|
||||
vol.Schema({vol.Required("user_id"): str, vol.Required("pin"): str})
|
||||
]
|
||||
},
|
||||
extra=vol.PREVENT_EXTRA,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@MULTI_FACTOR_AUTH_MODULES.register('insecure_example')
|
||||
@MULTI_FACTOR_AUTH_MODULES.register("insecure_example")
|
||||
class InsecureExampleModule(MultiFactorAuthModule):
|
||||
"""Example auth module validate pin."""
|
||||
|
||||
DEFAULT_TITLE = 'Insecure Personal Identify Number'
|
||||
DEFAULT_TITLE = "Insecure Personal Identify Number"
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: Dict[str, Any]) -> None:
|
||||
"""Initialize the user data store."""
|
||||
super().__init__(hass, config)
|
||||
self._data = config['data']
|
||||
self._data = config["data"]
|
||||
|
||||
@property
|
||||
def input_schema(self) -> vol.Schema:
|
||||
"""Validate login flow input data."""
|
||||
return vol.Schema({'pin': str})
|
||||
return vol.Schema({"pin": str})
|
||||
|
||||
@property
|
||||
def setup_schema(self) -> vol.Schema:
|
||||
"""Validate async_setup_user input data."""
|
||||
return vol.Schema({'pin': str})
|
||||
return vol.Schema({"pin": str})
|
||||
|
||||
async def async_setup_flow(self, user_id: str) -> SetupFlow:
|
||||
"""Return a data entry flow handler for setup module.
|
||||
|
|
@ -50,21 +56,21 @@ class InsecureExampleModule(MultiFactorAuthModule):
|
|||
async def async_setup_user(self, user_id: str, setup_data: Any) -> Any:
|
||||
"""Set up user to use mfa module."""
|
||||
# data shall has been validate in caller
|
||||
pin = setup_data['pin']
|
||||
pin = setup_data["pin"]
|
||||
|
||||
for data in self._data:
|
||||
if data['user_id'] == user_id:
|
||||
if data["user_id"] == user_id:
|
||||
# already setup, override
|
||||
data['pin'] = pin
|
||||
data["pin"] = pin
|
||||
return
|
||||
|
||||
self._data.append({'user_id': user_id, 'pin': pin})
|
||||
self._data.append({"user_id": user_id, "pin": pin})
|
||||
|
||||
async def async_depose_user(self, user_id: str) -> None:
|
||||
"""Remove user from mfa module."""
|
||||
found = None
|
||||
for data in self._data:
|
||||
if data['user_id'] == user_id:
|
||||
if data["user_id"] == user_id:
|
||||
found = data
|
||||
break
|
||||
if found:
|
||||
|
|
@ -73,17 +79,16 @@ class InsecureExampleModule(MultiFactorAuthModule):
|
|||
async def async_is_user_setup(self, user_id: str) -> bool:
|
||||
"""Return whether user is setup."""
|
||||
for data in self._data:
|
||||
if data['user_id'] == user_id:
|
||||
if data["user_id"] == user_id:
|
||||
return True
|
||||
return False
|
||||
|
||||
async def async_validation(
|
||||
self, user_id: str, user_input: Dict[str, Any]) -> bool:
|
||||
async def async_validation(self, user_id: str, user_input: Dict[str, Any]) -> bool:
|
||||
"""Return True if validation passed."""
|
||||
for data in self._data:
|
||||
if data['user_id'] == user_id:
|
||||
if data["user_id"] == user_id:
|
||||
# user_input has been validate in caller
|
||||
if data['pin'] == user_input['pin']:
|
||||
if data["pin"] == user_input["pin"]:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
|
|
|||
|
|
@ -8,23 +8,26 @@ import voluptuous as vol
|
|||
from homeassistant.auth.models import User
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import MultiFactorAuthModule, MULTI_FACTOR_AUTH_MODULES, \
|
||||
MULTI_FACTOR_AUTH_MODULE_SCHEMA, SetupFlow
|
||||
from . import (
|
||||
MultiFactorAuthModule,
|
||||
MULTI_FACTOR_AUTH_MODULES,
|
||||
MULTI_FACTOR_AUTH_MODULE_SCHEMA,
|
||||
SetupFlow,
|
||||
)
|
||||
|
||||
REQUIREMENTS = ['pyotp==2.2.6', 'PyQRCode==1.2.1']
|
||||
REQUIREMENTS = ["pyotp==2.2.6", "PyQRCode==1.2.1"]
|
||||
|
||||
CONFIG_SCHEMA = MULTI_FACTOR_AUTH_MODULE_SCHEMA.extend({
|
||||
}, extra=vol.PREVENT_EXTRA)
|
||||
CONFIG_SCHEMA = MULTI_FACTOR_AUTH_MODULE_SCHEMA.extend({}, extra=vol.PREVENT_EXTRA)
|
||||
|
||||
STORAGE_VERSION = 1
|
||||
STORAGE_KEY = 'auth_module.totp'
|
||||
STORAGE_USERS = 'users'
|
||||
STORAGE_USER_ID = 'user_id'
|
||||
STORAGE_OTA_SECRET = 'ota_secret'
|
||||
STORAGE_KEY = "auth_module.totp"
|
||||
STORAGE_USERS = "users"
|
||||
STORAGE_USER_ID = "user_id"
|
||||
STORAGE_OTA_SECRET = "ota_secret"
|
||||
|
||||
INPUT_FIELD_CODE = 'code'
|
||||
INPUT_FIELD_CODE = "code"
|
||||
|
||||
DUMMY_SECRET = 'FPPTH34D4E3MI2HG'
|
||||
DUMMY_SECRET = "FPPTH34D4E3MI2HG"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -37,10 +40,15 @@ def _generate_qr_code(data: str) -> str:
|
|||
|
||||
with BytesIO() as buffer:
|
||||
qr_code.svg(file=buffer, scale=4)
|
||||
return '{}'.format(
|
||||
buffer.getvalue().decode("ascii").replace('\n', '')
|
||||
.replace('<?xml version="1.0" encoding="UTF-8"?>'
|
||||
'<svg xmlns="http://www.w3.org/2000/svg"', '<svg')
|
||||
return "{}".format(
|
||||
buffer.getvalue()
|
||||
.decode("ascii")
|
||||
.replace("\n", "")
|
||||
.replace(
|
||||
'<?xml version="1.0" encoding="UTF-8"?>'
|
||||
'<svg xmlns="http://www.w3.org/2000/svg"',
|
||||
"<svg",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -50,23 +58,23 @@ def _generate_secret_and_qr_code(username: str) -> Tuple[str, str, str]:
|
|||
|
||||
ota_secret = pyotp.random_base32()
|
||||
url = pyotp.totp.TOTP(ota_secret).provisioning_uri(
|
||||
username, issuer_name="Home Assistant")
|
||||
username, issuer_name="Home Assistant"
|
||||
)
|
||||
image = _generate_qr_code(url)
|
||||
return ota_secret, url, image
|
||||
|
||||
|
||||
@MULTI_FACTOR_AUTH_MODULES.register('totp')
|
||||
@MULTI_FACTOR_AUTH_MODULES.register("totp")
|
||||
class TotpAuthModule(MultiFactorAuthModule):
|
||||
"""Auth module validate time-based one time password."""
|
||||
|
||||
DEFAULT_TITLE = 'Time-based One Time Password'
|
||||
DEFAULT_TITLE = "Time-based One Time Password"
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: Dict[str, Any]) -> None:
|
||||
"""Initialize the user data store."""
|
||||
super().__init__(hass, config)
|
||||
self._users = None # type: Optional[Dict[str, str]]
|
||||
self._user_store = hass.helpers.storage.Store(
|
||||
STORAGE_VERSION, STORAGE_KEY)
|
||||
self._user_store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
|
||||
|
||||
@property
|
||||
def input_schema(self) -> vol.Schema:
|
||||
|
|
@ -86,8 +94,7 @@ class TotpAuthModule(MultiFactorAuthModule):
|
|||
"""Save data."""
|
||||
await self._user_store.async_save({STORAGE_USERS: self._users})
|
||||
|
||||
def _add_ota_secret(self, user_id: str,
|
||||
secret: Optional[str] = None) -> str:
|
||||
def _add_ota_secret(self, user_id: str, secret: Optional[str] = None) -> str:
|
||||
"""Create a ota_secret for user."""
|
||||
import pyotp
|
||||
|
||||
|
|
@ -110,7 +117,8 @@ class TotpAuthModule(MultiFactorAuthModule):
|
|||
await self._async_load()
|
||||
|
||||
result = await self.hass.async_add_executor_job(
|
||||
self._add_ota_secret, user_id, setup_data.get('secret'))
|
||||
self._add_ota_secret, user_id, setup_data.get("secret")
|
||||
)
|
||||
|
||||
await self._async_save()
|
||||
return result
|
||||
|
|
@ -130,8 +138,7 @@ class TotpAuthModule(MultiFactorAuthModule):
|
|||
|
||||
return user_id in self._users # type: ignore
|
||||
|
||||
async def async_validation(
|
||||
self, user_id: str, user_input: Dict[str, Any]) -> bool:
|
||||
async def async_validation(self, user_id: str, user_input: Dict[str, Any]) -> bool:
|
||||
"""Return True if validation passed."""
|
||||
if self._users is None:
|
||||
await self._async_load()
|
||||
|
|
@ -139,7 +146,8 @@ class TotpAuthModule(MultiFactorAuthModule):
|
|||
# user_input has been validate in caller
|
||||
# set INPUT_FIELD_CODE as vol.Required is not user friendly
|
||||
return await self.hass.async_add_executor_job(
|
||||
self._validate_2fa, user_id, user_input.get(INPUT_FIELD_CODE, ''))
|
||||
self._validate_2fa, user_id, user_input.get(INPUT_FIELD_CODE, "")
|
||||
)
|
||||
|
||||
def _validate_2fa(self, user_id: str, code: str) -> bool:
|
||||
"""Validate two factor authentication code."""
|
||||
|
|
@ -158,9 +166,9 @@ class TotpAuthModule(MultiFactorAuthModule):
|
|||
class TotpSetupFlow(SetupFlow):
|
||||
"""Handler for the setup flow."""
|
||||
|
||||
def __init__(self, auth_module: TotpAuthModule,
|
||||
setup_schema: vol.Schema,
|
||||
user: User) -> None:
|
||||
def __init__(
|
||||
self, auth_module: TotpAuthModule, setup_schema: vol.Schema, user: User
|
||||
) -> None:
|
||||
"""Initialize the setup flow."""
|
||||
super().__init__(auth_module, setup_schema, user.id)
|
||||
# to fix typing complaint
|
||||
|
|
@ -171,8 +179,8 @@ class TotpSetupFlow(SetupFlow):
|
|||
self._image = None # type Optional[str]
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: Optional[Dict[str, str]] = None) \
|
||||
-> Dict[str, Any]:
|
||||
self, user_input: Optional[Dict[str, str]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Handle the first step of setup flow.
|
||||
|
||||
Return self.async_show_form(step_id='init') if user_input == None.
|
||||
|
|
@ -184,30 +192,31 @@ class TotpSetupFlow(SetupFlow):
|
|||
|
||||
if user_input:
|
||||
verified = await self.hass.async_add_executor_job( # type: ignore
|
||||
pyotp.TOTP(self._ota_secret).verify, user_input['code'])
|
||||
pyotp.TOTP(self._ota_secret).verify, user_input["code"]
|
||||
)
|
||||
if verified:
|
||||
result = await self._auth_module.async_setup_user(
|
||||
self._user_id, {'secret': self._ota_secret})
|
||||
self._user_id, {"secret": self._ota_secret}
|
||||
)
|
||||
return self.async_create_entry(
|
||||
title=self._auth_module.name,
|
||||
data={'result': result}
|
||||
title=self._auth_module.name, data={"result": result}
|
||||
)
|
||||
|
||||
errors['base'] = 'invalid_code'
|
||||
errors["base"] = "invalid_code"
|
||||
|
||||
else:
|
||||
hass = self._auth_module.hass
|
||||
self._ota_secret, self._url, self._image = \
|
||||
await hass.async_add_executor_job( # type: ignore
|
||||
_generate_secret_and_qr_code, str(self._user.name))
|
||||
self._ota_secret, self._url, self._image = await hass.async_add_executor_job( # type: ignore
|
||||
_generate_secret_and_qr_code, str(self._user.name)
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id='init',
|
||||
step_id="init",
|
||||
data_schema=self._setup_schema,
|
||||
description_placeholders={
|
||||
'code': self._ota_secret,
|
||||
'url': self._url,
|
||||
'qr_code': self._image
|
||||
"code": self._ota_secret,
|
||||
"url": self._url,
|
||||
"qr_code": self._image,
|
||||
},
|
||||
errors=errors
|
||||
errors=errors,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -9,9 +9,9 @@ from homeassistant.util import dt as dt_util
|
|||
|
||||
from .util import generate_secret
|
||||
|
||||
TOKEN_TYPE_NORMAL = 'normal'
|
||||
TOKEN_TYPE_SYSTEM = 'system'
|
||||
TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN = 'long_lived_access_token'
|
||||
TOKEN_TYPE_NORMAL = "normal"
|
||||
TOKEN_TYPE_SYSTEM = "system"
|
||||
TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN = "long_lived_access_token"
|
||||
|
||||
|
||||
@attr.s(slots=True)
|
||||
|
|
@ -44,16 +44,17 @@ class RefreshToken:
|
|||
access_token_expiration = attr.ib(type=timedelta)
|
||||
client_name = attr.ib(type=Optional[str], default=None)
|
||||
client_icon = attr.ib(type=Optional[str], default=None)
|
||||
token_type = attr.ib(type=str, default=TOKEN_TYPE_NORMAL,
|
||||
validator=attr.validators.in_((
|
||||
TOKEN_TYPE_NORMAL, TOKEN_TYPE_SYSTEM,
|
||||
TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN)))
|
||||
token_type = attr.ib(
|
||||
type=str,
|
||||
default=TOKEN_TYPE_NORMAL,
|
||||
validator=attr.validators.in_(
|
||||
(TOKEN_TYPE_NORMAL, TOKEN_TYPE_SYSTEM, TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN)
|
||||
),
|
||||
)
|
||||
id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex))
|
||||
created_at = attr.ib(type=datetime, default=attr.Factory(dt_util.utcnow))
|
||||
token = attr.ib(type=str,
|
||||
default=attr.Factory(lambda: generate_secret(64)))
|
||||
jwt_key = attr.ib(type=str,
|
||||
default=attr.Factory(lambda: generate_secret(64)))
|
||||
token = attr.ib(type=str, default=attr.Factory(lambda: generate_secret(64)))
|
||||
jwt_key = attr.ib(type=str, default=attr.Factory(lambda: generate_secret(64)))
|
||||
|
||||
last_used_at = attr.ib(type=Optional[datetime], default=None)
|
||||
last_used_ip = attr.ib(type=Optional[str], default=None)
|
||||
|
|
@ -73,5 +74,4 @@ class Credentials:
|
|||
is_new = attr.ib(type=bool, default=True)
|
||||
|
||||
|
||||
UserMeta = NamedTuple("UserMeta",
|
||||
[('name', Optional[str]), ('is_active', bool)])
|
||||
UserMeta = NamedTuple("UserMeta", [("name", Optional[str]), ("is_active", bool)])
|
||||
|
|
|
|||
|
|
@ -19,25 +19,29 @@ from ..models import Credentials, User, UserMeta # noqa: F401
|
|||
from ..mfa_modules import SESSION_EXPIRATION
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
DATA_REQS = 'auth_prov_reqs_processed'
|
||||
DATA_REQS = "auth_prov_reqs_processed"
|
||||
|
||||
AUTH_PROVIDERS = Registry()
|
||||
|
||||
AUTH_PROVIDER_SCHEMA = vol.Schema({
|
||||
AUTH_PROVIDER_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_TYPE): str,
|
||||
vol.Optional(CONF_NAME): str,
|
||||
# Specify ID if you have two auth providers for same type.
|
||||
vol.Optional(CONF_ID): str,
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
|
||||
class AuthProvider:
|
||||
"""Provider of user authentication."""
|
||||
|
||||
DEFAULT_TITLE = 'Unnamed auth provider'
|
||||
DEFAULT_TITLE = "Unnamed auth provider"
|
||||
|
||||
def __init__(self, hass: HomeAssistant, store: AuthStore,
|
||||
config: Dict[str, Any]) -> None:
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, store: AuthStore, config: Dict[str, Any]
|
||||
) -> None:
|
||||
"""Initialize an auth provider."""
|
||||
self.hass = hass
|
||||
self.store = store
|
||||
|
|
@ -73,22 +77,22 @@ class AuthProvider:
|
|||
credentials
|
||||
for user in users
|
||||
for credentials in user.credentials
|
||||
if (credentials.auth_provider_type == self.type and
|
||||
credentials.auth_provider_id == self.id)
|
||||
if (
|
||||
credentials.auth_provider_type == self.type
|
||||
and credentials.auth_provider_id == self.id
|
||||
)
|
||||
]
|
||||
|
||||
@callback
|
||||
def async_create_credentials(self, data: Dict[str, str]) -> Credentials:
|
||||
"""Create credentials."""
|
||||
return Credentials(
|
||||
auth_provider_type=self.type,
|
||||
auth_provider_id=self.id,
|
||||
data=data,
|
||||
auth_provider_type=self.type, auth_provider_id=self.id, data=data
|
||||
)
|
||||
|
||||
# Implement by extending class
|
||||
|
||||
async def async_login_flow(self, context: Optional[Dict]) -> 'LoginFlow':
|
||||
async def async_login_flow(self, context: Optional[Dict]) -> "LoginFlow":
|
||||
"""Return the data flow for logging in with auth provider.
|
||||
|
||||
Auth provider should extend LoginFlow and return an instance.
|
||||
|
|
@ -96,12 +100,14 @@ class AuthProvider:
|
|||
raise NotImplementedError
|
||||
|
||||
async def async_get_or_create_credentials(
|
||||
self, flow_result: Dict[str, str]) -> Credentials:
|
||||
self, flow_result: Dict[str, str]
|
||||
) -> Credentials:
|
||||
"""Get credentials based on the flow result."""
|
||||
raise NotImplementedError
|
||||
|
||||
async def async_user_meta_for_credentials(
|
||||
self, credentials: Credentials) -> UserMeta:
|
||||
self, credentials: Credentials
|
||||
) -> UserMeta:
|
||||
"""Return extra user metadata for credentials.
|
||||
|
||||
Will be used to populate info when creating a new user.
|
||||
|
|
@ -110,8 +116,8 @@ class AuthProvider:
|
|||
|
||||
|
||||
async def auth_provider_from_config(
|
||||
hass: HomeAssistant, store: AuthStore,
|
||||
config: Dict[str, Any]) -> AuthProvider:
|
||||
hass: HomeAssistant, store: AuthStore, config: Dict[str, Any]
|
||||
) -> AuthProvider:
|
||||
"""Initialize an auth provider from a config."""
|
||||
provider_name = config[CONF_TYPE]
|
||||
module = await load_auth_provider_module(hass, provider_name)
|
||||
|
|
@ -119,25 +125,31 @@ async def auth_provider_from_config(
|
|||
try:
|
||||
config = module.CONFIG_SCHEMA(config) # type: ignore
|
||||
except vol.Invalid as err:
|
||||
_LOGGER.error('Invalid configuration for auth provider %s: %s',
|
||||
provider_name, humanize_error(config, err))
|
||||
_LOGGER.error(
|
||||
"Invalid configuration for auth provider %s: %s",
|
||||
provider_name,
|
||||
humanize_error(config, err),
|
||||
)
|
||||
raise
|
||||
|
||||
return AUTH_PROVIDERS[provider_name](hass, store, config) # type: ignore
|
||||
|
||||
|
||||
async def load_auth_provider_module(
|
||||
hass: HomeAssistant, provider: str) -> types.ModuleType:
|
||||
hass: HomeAssistant, provider: str
|
||||
) -> types.ModuleType:
|
||||
"""Load an auth provider."""
|
||||
try:
|
||||
module = importlib.import_module(
|
||||
'homeassistant.auth.providers.{}'.format(provider))
|
||||
"homeassistant.auth.providers.{}".format(provider)
|
||||
)
|
||||
except ImportError as err:
|
||||
_LOGGER.error('Unable to load auth provider %s: %s', provider, err)
|
||||
raise HomeAssistantError('Unable to load auth provider {}: {}'.format(
|
||||
provider, err))
|
||||
_LOGGER.error("Unable to load auth provider %s: %s", provider, err)
|
||||
raise HomeAssistantError(
|
||||
"Unable to load auth provider {}: {}".format(provider, err)
|
||||
)
|
||||
|
||||
if hass.config.skip_pip or not hasattr(module, 'REQUIREMENTS'):
|
||||
if hass.config.skip_pip or not hasattr(module, "REQUIREMENTS"):
|
||||
return module
|
||||
|
||||
processed = hass.data.get(DATA_REQS)
|
||||
|
|
@ -150,12 +162,13 @@ async def load_auth_provider_module(
|
|||
# https://github.com/python/mypy/issues/1424
|
||||
reqs = module.REQUIREMENTS # type: ignore
|
||||
req_success = await requirements.async_process_requirements(
|
||||
hass, 'auth provider {}'.format(provider), reqs)
|
||||
hass, "auth provider {}".format(provider), reqs
|
||||
)
|
||||
|
||||
if not req_success:
|
||||
raise HomeAssistantError(
|
||||
'Unable to process requirements of auth provider {}'.format(
|
||||
provider))
|
||||
"Unable to process requirements of auth provider {}".format(provider)
|
||||
)
|
||||
|
||||
processed.add(provider)
|
||||
return module
|
||||
|
|
@ -174,8 +187,8 @@ class LoginFlow(data_entry_flow.FlowHandler):
|
|||
self.user = None # type: Optional[User]
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: Optional[Dict[str, str]] = None) \
|
||||
-> Dict[str, Any]:
|
||||
self, user_input: Optional[Dict[str, str]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Handle the first step of login flow.
|
||||
|
||||
Return self.async_show_form(step_id='init') if user_input == None.
|
||||
|
|
@ -184,38 +197,37 @@ class LoginFlow(data_entry_flow.FlowHandler):
|
|||
raise NotImplementedError
|
||||
|
||||
async def async_step_select_mfa_module(
|
||||
self, user_input: Optional[Dict[str, str]] = None) \
|
||||
-> Dict[str, Any]:
|
||||
self, user_input: Optional[Dict[str, str]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Handle the step of select mfa module."""
|
||||
errors = {}
|
||||
|
||||
if user_input is not None:
|
||||
auth_module = user_input.get('multi_factor_auth_module')
|
||||
auth_module = user_input.get("multi_factor_auth_module")
|
||||
if auth_module in self.available_mfa_modules:
|
||||
self._auth_module_id = auth_module
|
||||
return await self.async_step_mfa()
|
||||
errors['base'] = 'invalid_auth_module'
|
||||
errors["base"] = "invalid_auth_module"
|
||||
|
||||
if len(self.available_mfa_modules) == 1:
|
||||
self._auth_module_id = list(self.available_mfa_modules.keys())[0]
|
||||
return await self.async_step_mfa()
|
||||
|
||||
return self.async_show_form(
|
||||
step_id='select_mfa_module',
|
||||
data_schema=vol.Schema({
|
||||
'multi_factor_auth_module': vol.In(self.available_mfa_modules)
|
||||
}),
|
||||
step_id="select_mfa_module",
|
||||
data_schema=vol.Schema(
|
||||
{"multi_factor_auth_module": vol.In(self.available_mfa_modules)}
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_mfa(
|
||||
self, user_input: Optional[Dict[str, str]] = None) \
|
||||
-> Dict[str, Any]:
|
||||
self, user_input: Optional[Dict[str, str]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Handle the step of mfa validation."""
|
||||
errors = {}
|
||||
|
||||
auth_module = self._auth_manager.get_auth_mfa_module(
|
||||
self._auth_module_id)
|
||||
auth_module = self._auth_manager.get_auth_mfa_module(self._auth_module_id)
|
||||
if auth_module is None:
|
||||
# Given an invalid input to async_step_select_mfa_module
|
||||
# will show invalid_auth_module error
|
||||
|
|
@ -224,25 +236,24 @@ class LoginFlow(data_entry_flow.FlowHandler):
|
|||
if user_input is not None:
|
||||
expires = self.created_at + SESSION_EXPIRATION
|
||||
if dt_util.utcnow() > expires:
|
||||
return self.async_abort(
|
||||
reason='login_expired'
|
||||
)
|
||||
return self.async_abort(reason="login_expired")
|
||||
|
||||
result = await auth_module.async_validation(
|
||||
self.user.id, user_input) # type: ignore
|
||||
self.user.id, user_input
|
||||
) # type: ignore
|
||||
if not result:
|
||||
errors['base'] = 'invalid_code'
|
||||
errors["base"] = "invalid_code"
|
||||
|
||||
if not errors:
|
||||
return await self.async_finish(self.user)
|
||||
|
||||
description_placeholders = {
|
||||
'mfa_module_name': auth_module.name,
|
||||
'mfa_module_id': auth_module.id
|
||||
"mfa_module_name": auth_module.name,
|
||||
"mfa_module_id": auth_module.id,
|
||||
} # type: Dict[str, str]
|
||||
|
||||
return self.async_show_form(
|
||||
step_id='mfa',
|
||||
step_id="mfa",
|
||||
data_schema=auth_module.input_schema,
|
||||
description_placeholders=description_placeholders,
|
||||
errors=errors,
|
||||
|
|
@ -250,7 +261,4 @@ class LoginFlow(data_entry_flow.FlowHandler):
|
|||
|
||||
async def async_finish(self, flow_result: Any) -> Dict:
|
||||
"""Handle the pass of login flow."""
|
||||
return self.async_create_entry(
|
||||
title=self._auth_provider.name,
|
||||
data=flow_result
|
||||
)
|
||||
return self.async_create_entry(title=self._auth_provider.name, data=flow_result)
|
||||
|
|
|
|||
|
|
@ -20,14 +20,13 @@ from ..util import generate_secret
|
|||
|
||||
|
||||
STORAGE_VERSION = 1
|
||||
STORAGE_KEY = 'auth_provider.homeassistant'
|
||||
STORAGE_KEY = "auth_provider.homeassistant"
|
||||
|
||||
|
||||
def _disallow_id(conf: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Disallow ID in config."""
|
||||
if CONF_ID in conf:
|
||||
raise vol.Invalid(
|
||||
'ID is not allowed for the homeassistant auth provider.')
|
||||
raise vol.Invalid("ID is not allowed for the homeassistant auth provider.")
|
||||
|
||||
return conf
|
||||
|
||||
|
|
@ -60,68 +59,62 @@ class Data:
|
|||
data = await self._store.async_load()
|
||||
|
||||
if data is None:
|
||||
data = {
|
||||
'salt': generate_secret(),
|
||||
'users': []
|
||||
}
|
||||
data = {"salt": generate_secret(), "users": []}
|
||||
|
||||
self._data = data
|
||||
|
||||
@property
|
||||
def users(self) -> List[Dict[str, str]]:
|
||||
"""Return users."""
|
||||
return self._data['users'] # type: ignore
|
||||
return self._data["users"] # type: ignore
|
||||
|
||||
def validate_login(self, username: str, password: str) -> None:
|
||||
"""Validate a username and password.
|
||||
|
||||
Raises InvalidAuth if auth invalid.
|
||||
"""
|
||||
dummy = b'$2b$12$CiuFGszHx9eNHxPuQcwBWez4CwDTOcLTX5CbOpV6gef2nYuXkY7BO'
|
||||
dummy = b"$2b$12$CiuFGszHx9eNHxPuQcwBWez4CwDTOcLTX5CbOpV6gef2nYuXkY7BO"
|
||||
found = None
|
||||
|
||||
# Compare all users to avoid timing attacks.
|
||||
for user in self.users:
|
||||
if username == user['username']:
|
||||
if username == user["username"]:
|
||||
found = user
|
||||
|
||||
if found is None:
|
||||
# check a hash to make timing the same as if user was found
|
||||
bcrypt.checkpw(b'foo',
|
||||
dummy)
|
||||
bcrypt.checkpw(b"foo", dummy)
|
||||
raise InvalidAuth
|
||||
|
||||
user_hash = base64.b64decode(found['password'])
|
||||
user_hash = base64.b64decode(found["password"])
|
||||
|
||||
# if the hash is not a bcrypt hash...
|
||||
# provide a transparant upgrade for old pbkdf2 hash format
|
||||
if not (user_hash.startswith(b'$2a$')
|
||||
or user_hash.startswith(b'$2b$')
|
||||
or user_hash.startswith(b'$2x$')
|
||||
or user_hash.startswith(b'$2y$')):
|
||||
if not (
|
||||
user_hash.startswith(b"$2a$")
|
||||
or user_hash.startswith(b"$2b$")
|
||||
or user_hash.startswith(b"$2x$")
|
||||
or user_hash.startswith(b"$2y$")
|
||||
):
|
||||
# IMPORTANT! validate the login, bail if invalid
|
||||
hashed = self.legacy_hash_password(password)
|
||||
if not hmac.compare_digest(hashed, user_hash):
|
||||
raise InvalidAuth
|
||||
# then re-hash the valid password with bcrypt
|
||||
self.change_password(found['username'], password)
|
||||
run_coroutine_threadsafe(
|
||||
self.async_save(), self.hass.loop
|
||||
).result()
|
||||
user_hash = base64.b64decode(found['password'])
|
||||
self.change_password(found["username"], password)
|
||||
run_coroutine_threadsafe(self.async_save(), self.hass.loop).result()
|
||||
user_hash = base64.b64decode(found["password"])
|
||||
|
||||
# bcrypt.checkpw is timing-safe
|
||||
if not bcrypt.checkpw(password.encode(),
|
||||
user_hash):
|
||||
if not bcrypt.checkpw(password.encode(), user_hash):
|
||||
raise InvalidAuth
|
||||
|
||||
def legacy_hash_password(self, password: str,
|
||||
for_storage: bool = False) -> bytes:
|
||||
def legacy_hash_password(self, password: str, for_storage: bool = False) -> bytes:
|
||||
"""LEGACY password encoding."""
|
||||
# We're no longer storing salts in data, but if one exists we
|
||||
# should be able to retrieve it.
|
||||
salt = self._data['salt'].encode() # type: ignore
|
||||
hashed = hashlib.pbkdf2_hmac('sha512', password.encode(), salt, 100000)
|
||||
salt = self._data["salt"].encode() # type: ignore
|
||||
hashed = hashlib.pbkdf2_hmac("sha512", password.encode(), salt, 100000)
|
||||
if for_storage:
|
||||
hashed = base64.b64encode(hashed)
|
||||
return hashed
|
||||
|
|
@ -129,7 +122,7 @@ class Data:
|
|||
# pylint: disable=no-self-use
|
||||
def hash_password(self, password: str, for_storage: bool = False) -> bytes:
|
||||
"""Encode a password."""
|
||||
hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12)) \
|
||||
hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12))
|
||||
# type: bytes
|
||||
if for_storage:
|
||||
hashed = base64.b64encode(hashed)
|
||||
|
|
@ -137,20 +130,22 @@ class Data:
|
|||
|
||||
def add_auth(self, username: str, password: str) -> None:
|
||||
"""Add a new authenticated user/pass."""
|
||||
if any(user['username'] == username for user in self.users):
|
||||
if any(user["username"] == username for user in self.users):
|
||||
raise InvalidUser
|
||||
|
||||
self.users.append({
|
||||
'username': username,
|
||||
'password': self.hash_password(password, True).decode(),
|
||||
})
|
||||
self.users.append(
|
||||
{
|
||||
"username": username,
|
||||
"password": self.hash_password(password, True).decode(),
|
||||
}
|
||||
)
|
||||
|
||||
@callback
|
||||
def async_remove_auth(self, username: str) -> None:
|
||||
"""Remove authentication."""
|
||||
index = None
|
||||
for i, user in enumerate(self.users):
|
||||
if user['username'] == username:
|
||||
if user["username"] == username:
|
||||
index = i
|
||||
break
|
||||
|
||||
|
|
@ -165,9 +160,8 @@ class Data:
|
|||
Raises InvalidUser if user cannot be found.
|
||||
"""
|
||||
for user in self.users:
|
||||
if user['username'] == username:
|
||||
user['password'] = self.hash_password(
|
||||
new_password, True).decode()
|
||||
if user["username"] == username:
|
||||
user["password"] = self.hash_password(new_password, True).decode()
|
||||
break
|
||||
else:
|
||||
raise InvalidUser
|
||||
|
|
@ -177,11 +171,11 @@ class Data:
|
|||
await self._store.async_save(self._data)
|
||||
|
||||
|
||||
@AUTH_PROVIDERS.register('homeassistant')
|
||||
@AUTH_PROVIDERS.register("homeassistant")
|
||||
class HassAuthProvider(AuthProvider):
|
||||
"""Auth provider based on a local storage of users in HASS config dir."""
|
||||
|
||||
DEFAULT_TITLE = 'Home Assistant Local'
|
||||
DEFAULT_TITLE = "Home Assistant Local"
|
||||
|
||||
data = None
|
||||
|
||||
|
|
@ -193,8 +187,7 @@ class HassAuthProvider(AuthProvider):
|
|||
self.data = Data(self.hass)
|
||||
await self.data.async_load()
|
||||
|
||||
async def async_login_flow(
|
||||
self, context: Optional[Dict]) -> LoginFlow:
|
||||
async def async_login_flow(self, context: Optional[Dict]) -> LoginFlow:
|
||||
"""Return a flow to login."""
|
||||
return HassLoginFlow(self)
|
||||
|
||||
|
|
@ -205,36 +198,36 @@ class HassAuthProvider(AuthProvider):
|
|||
assert self.data is not None
|
||||
|
||||
await self.hass.async_add_executor_job(
|
||||
self.data.validate_login, username, password)
|
||||
self.data.validate_login, username, password
|
||||
)
|
||||
|
||||
async def async_get_or_create_credentials(
|
||||
self, flow_result: Dict[str, str]) -> Credentials:
|
||||
self, flow_result: Dict[str, str]
|
||||
) -> Credentials:
|
||||
"""Get credentials based on the flow result."""
|
||||
username = flow_result['username']
|
||||
username = flow_result["username"]
|
||||
|
||||
for credential in await self.async_credentials():
|
||||
if credential.data['username'] == username:
|
||||
if credential.data["username"] == username:
|
||||
return credential
|
||||
|
||||
# Create new credentials.
|
||||
return self.async_create_credentials({
|
||||
'username': username
|
||||
})
|
||||
return self.async_create_credentials({"username": username})
|
||||
|
||||
async def async_user_meta_for_credentials(
|
||||
self, credentials: Credentials) -> UserMeta:
|
||||
self, credentials: Credentials
|
||||
) -> UserMeta:
|
||||
"""Get extra info for this credential."""
|
||||
return UserMeta(name=credentials.data['username'], is_active=True)
|
||||
return UserMeta(name=credentials.data["username"], is_active=True)
|
||||
|
||||
async def async_will_remove_credentials(
|
||||
self, credentials: Credentials) -> None:
|
||||
async def async_will_remove_credentials(self, credentials: Credentials) -> None:
|
||||
"""When credentials get removed, also remove the auth."""
|
||||
if self.data is None:
|
||||
await self.async_initialize()
|
||||
assert self.data is not None
|
||||
|
||||
try:
|
||||
self.data.async_remove_auth(credentials.data['username'])
|
||||
self.data.async_remove_auth(credentials.data["username"])
|
||||
await self.data.async_save()
|
||||
except InvalidUser:
|
||||
# Can happen if somehow we didn't clean up a credential
|
||||
|
|
@ -245,29 +238,27 @@ class HassLoginFlow(LoginFlow):
|
|||
"""Handler for the login flow."""
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: Optional[Dict[str, str]] = None) \
|
||||
-> Dict[str, Any]:
|
||||
self, user_input: Optional[Dict[str, str]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Handle the step of the form."""
|
||||
errors = {}
|
||||
|
||||
if user_input is not None:
|
||||
try:
|
||||
await cast(HassAuthProvider, self._auth_provider)\
|
||||
.async_validate_login(user_input['username'],
|
||||
user_input['password'])
|
||||
await cast(HassAuthProvider, self._auth_provider).async_validate_login(
|
||||
user_input["username"], user_input["password"]
|
||||
)
|
||||
except InvalidAuth:
|
||||
errors['base'] = 'invalid_auth'
|
||||
errors["base"] = "invalid_auth"
|
||||
|
||||
if not errors:
|
||||
user_input.pop('password')
|
||||
user_input.pop("password")
|
||||
return await self.async_finish(user_input)
|
||||
|
||||
schema = OrderedDict() # type: Dict[str, type]
|
||||
schema['username'] = str
|
||||
schema['password'] = str
|
||||
schema["username"] = str
|
||||
schema["password"] = str
|
||||
|
||||
return self.async_show_form(
|
||||
step_id='init',
|
||||
data_schema=vol.Schema(schema),
|
||||
errors=errors,
|
||||
step_id="init", data_schema=vol.Schema(schema), errors=errors
|
||||
)
|
||||
|
|
|
|||
|
|
@ -12,23 +12,25 @@ from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, LoginFlow
|
|||
from ..models import Credentials, UserMeta
|
||||
|
||||
|
||||
USER_SCHEMA = vol.Schema({
|
||||
vol.Required('username'): str,
|
||||
vol.Required('password'): str,
|
||||
vol.Optional('name'): str,
|
||||
})
|
||||
USER_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required("username"): str,
|
||||
vol.Required("password"): str,
|
||||
vol.Optional("name"): str,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend({
|
||||
vol.Required('users'): [USER_SCHEMA]
|
||||
}, extra=vol.PREVENT_EXTRA)
|
||||
CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend(
|
||||
{vol.Required("users"): [USER_SCHEMA]}, extra=vol.PREVENT_EXTRA
|
||||
)
|
||||
|
||||
|
||||
class InvalidAuthError(HomeAssistantError):
|
||||
"""Raised when submitting invalid authentication."""
|
||||
|
||||
|
||||
@AUTH_PROVIDERS.register('insecure_example')
|
||||
@AUTH_PROVIDERS.register("insecure_example")
|
||||
class ExampleAuthProvider(AuthProvider):
|
||||
"""Example auth provider based on hardcoded usernames and passwords."""
|
||||
|
||||
|
|
@ -42,47 +44,48 @@ class ExampleAuthProvider(AuthProvider):
|
|||
user = None
|
||||
|
||||
# Compare all users to avoid timing attacks.
|
||||
for usr in self.config['users']:
|
||||
if hmac.compare_digest(username.encode('utf-8'),
|
||||
usr['username'].encode('utf-8')):
|
||||
for usr in self.config["users"]:
|
||||
if hmac.compare_digest(
|
||||
username.encode("utf-8"), usr["username"].encode("utf-8")
|
||||
):
|
||||
user = usr
|
||||
|
||||
if user is None:
|
||||
# Do one more compare to make timing the same as if user was found.
|
||||
hmac.compare_digest(password.encode('utf-8'),
|
||||
password.encode('utf-8'))
|
||||
hmac.compare_digest(password.encode("utf-8"), password.encode("utf-8"))
|
||||
raise InvalidAuthError
|
||||
|
||||
if not hmac.compare_digest(user['password'].encode('utf-8'),
|
||||
password.encode('utf-8')):
|
||||
if not hmac.compare_digest(
|
||||
user["password"].encode("utf-8"), password.encode("utf-8")
|
||||
):
|
||||
raise InvalidAuthError
|
||||
|
||||
async def async_get_or_create_credentials(
|
||||
self, flow_result: Dict[str, str]) -> Credentials:
|
||||
self, flow_result: Dict[str, str]
|
||||
) -> Credentials:
|
||||
"""Get credentials based on the flow result."""
|
||||
username = flow_result['username']
|
||||
username = flow_result["username"]
|
||||
|
||||
for credential in await self.async_credentials():
|
||||
if credential.data['username'] == username:
|
||||
if credential.data["username"] == username:
|
||||
return credential
|
||||
|
||||
# Create new credentials.
|
||||
return self.async_create_credentials({
|
||||
'username': username
|
||||
})
|
||||
return self.async_create_credentials({"username": username})
|
||||
|
||||
async def async_user_meta_for_credentials(
|
||||
self, credentials: Credentials) -> UserMeta:
|
||||
self, credentials: Credentials
|
||||
) -> UserMeta:
|
||||
"""Return extra user metadata for credentials.
|
||||
|
||||
Will be used to populate info when creating a new user.
|
||||
"""
|
||||
username = credentials.data['username']
|
||||
username = credentials.data["username"]
|
||||
name = None
|
||||
|
||||
for user in self.config['users']:
|
||||
if user['username'] == username:
|
||||
name = user.get('name')
|
||||
for user in self.config["users"]:
|
||||
if user["username"] == username:
|
||||
name = user.get("name")
|
||||
break
|
||||
|
||||
return UserMeta(name=name, is_active=True)
|
||||
|
|
@ -92,29 +95,27 @@ class ExampleLoginFlow(LoginFlow):
|
|||
"""Handler for the login flow."""
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: Optional[Dict[str, str]] = None) \
|
||||
-> Dict[str, Any]:
|
||||
self, user_input: Optional[Dict[str, str]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Handle the step of the form."""
|
||||
errors = {}
|
||||
|
||||
if user_input is not None:
|
||||
try:
|
||||
cast(ExampleAuthProvider, self._auth_provider)\
|
||||
.async_validate_login(user_input['username'],
|
||||
user_input['password'])
|
||||
cast(ExampleAuthProvider, self._auth_provider).async_validate_login(
|
||||
user_input["username"], user_input["password"]
|
||||
)
|
||||
except InvalidAuthError:
|
||||
errors['base'] = 'invalid_auth'
|
||||
errors["base"] = "invalid_auth"
|
||||
|
||||
if not errors:
|
||||
user_input.pop('password')
|
||||
user_input.pop("password")
|
||||
return await self.async_finish(user_input)
|
||||
|
||||
schema = OrderedDict() # type: Dict[str, type]
|
||||
schema['username'] = str
|
||||
schema['password'] = str
|
||||
schema["username"] = str
|
||||
schema["password"] = str
|
||||
|
||||
return self.async_show_form(
|
||||
step_id='init',
|
||||
data_schema=vol.Schema(schema),
|
||||
errors=errors,
|
||||
step_id="init", data_schema=vol.Schema(schema), errors=errors
|
||||
)
|
||||
|
|
|
|||
|
|
@ -16,26 +16,23 @@ from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, LoginFlow
|
|||
from ..models import Credentials, UserMeta
|
||||
|
||||
|
||||
USER_SCHEMA = vol.Schema({
|
||||
vol.Required('username'): str,
|
||||
})
|
||||
USER_SCHEMA = vol.Schema({vol.Required("username"): str})
|
||||
|
||||
|
||||
CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend({
|
||||
}, extra=vol.PREVENT_EXTRA)
|
||||
CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend({}, extra=vol.PREVENT_EXTRA)
|
||||
|
||||
LEGACY_USER_NAME = 'Legacy API password user'
|
||||
LEGACY_USER_NAME = "Legacy API password user"
|
||||
|
||||
|
||||
class InvalidAuthError(HomeAssistantError):
|
||||
"""Raised when submitting invalid authentication."""
|
||||
|
||||
|
||||
@AUTH_PROVIDERS.register('legacy_api_password')
|
||||
@AUTH_PROVIDERS.register("legacy_api_password")
|
||||
class LegacyApiPasswordAuthProvider(AuthProvider):
|
||||
"""Example auth provider based on hardcoded usernames and passwords."""
|
||||
|
||||
DEFAULT_TITLE = 'Legacy API Password'
|
||||
DEFAULT_TITLE = "Legacy API Password"
|
||||
|
||||
async def async_login_flow(self, context: Optional[Dict]) -> LoginFlow:
|
||||
"""Return a flow to login."""
|
||||
|
|
@ -44,14 +41,16 @@ class LegacyApiPasswordAuthProvider(AuthProvider):
|
|||
@callback
|
||||
def async_validate_login(self, password: str) -> None:
|
||||
"""Validate a username and password."""
|
||||
hass_http = getattr(self.hass, 'http', None) # type: HomeAssistantHTTP
|
||||
hass_http = getattr(self.hass, "http", None) # type: HomeAssistantHTTP
|
||||
|
||||
if not hmac.compare_digest(hass_http.api_password.encode('utf-8'),
|
||||
password.encode('utf-8')):
|
||||
if not hmac.compare_digest(
|
||||
hass_http.api_password.encode("utf-8"), password.encode("utf-8")
|
||||
):
|
||||
raise InvalidAuthError
|
||||
|
||||
async def async_get_or_create_credentials(
|
||||
self, flow_result: Dict[str, str]) -> Credentials:
|
||||
self, flow_result: Dict[str, str]
|
||||
) -> Credentials:
|
||||
"""Return credentials for this login."""
|
||||
credentials = await self.async_credentials()
|
||||
if credentials:
|
||||
|
|
@ -60,7 +59,8 @@ class LegacyApiPasswordAuthProvider(AuthProvider):
|
|||
return self.async_create_credentials({})
|
||||
|
||||
async def async_user_meta_for_credentials(
|
||||
self, credentials: Credentials) -> UserMeta:
|
||||
self, credentials: Credentials
|
||||
) -> UserMeta:
|
||||
"""
|
||||
Return info for the user.
|
||||
|
||||
|
|
@ -73,29 +73,26 @@ class LegacyLoginFlow(LoginFlow):
|
|||
"""Handler for the login flow."""
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: Optional[Dict[str, str]] = None) \
|
||||
-> Dict[str, Any]:
|
||||
self, user_input: Optional[Dict[str, str]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Handle the step of the form."""
|
||||
errors = {}
|
||||
|
||||
hass_http = getattr(self.hass, 'http', None)
|
||||
hass_http = getattr(self.hass, "http", None)
|
||||
if hass_http is None or not hass_http.api_password:
|
||||
return self.async_abort(
|
||||
reason='no_api_password_set'
|
||||
)
|
||||
return self.async_abort(reason="no_api_password_set")
|
||||
|
||||
if user_input is not None:
|
||||
try:
|
||||
cast(LegacyApiPasswordAuthProvider, self._auth_provider)\
|
||||
.async_validate_login(user_input['password'])
|
||||
cast(
|
||||
LegacyApiPasswordAuthProvider, self._auth_provider
|
||||
).async_validate_login(user_input["password"])
|
||||
except InvalidAuthError:
|
||||
errors['base'] = 'invalid_auth'
|
||||
errors["base"] = "invalid_auth"
|
||||
|
||||
if not errors:
|
||||
return await self.async_finish({})
|
||||
|
||||
return self.async_show_form(
|
||||
step_id='init',
|
||||
data_schema=vol.Schema({'password': str}),
|
||||
errors=errors,
|
||||
step_id="init", data_schema=vol.Schema({"password": str}), errors=errors
|
||||
)
|
||||
|
|
|
|||
|
|
@ -14,8 +14,7 @@ from homeassistant.exceptions import HomeAssistantError
|
|||
from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, LoginFlow
|
||||
from ..models import Credentials, UserMeta
|
||||
|
||||
CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend({
|
||||
}, extra=vol.PREVENT_EXTRA)
|
||||
CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend({}, extra=vol.PREVENT_EXTRA)
|
||||
|
||||
|
||||
class InvalidAuthError(HomeAssistantError):
|
||||
|
|
@ -26,14 +25,14 @@ class InvalidUserError(HomeAssistantError):
|
|||
"""Raised when try to login as invalid user."""
|
||||
|
||||
|
||||
@AUTH_PROVIDERS.register('trusted_networks')
|
||||
@AUTH_PROVIDERS.register("trusted_networks")
|
||||
class TrustedNetworksAuthProvider(AuthProvider):
|
||||
"""Trusted Networks auth provider.
|
||||
|
||||
Allow passwordless access from trusted network.
|
||||
"""
|
||||
|
||||
DEFAULT_TITLE = 'Trusted Networks'
|
||||
DEFAULT_TITLE = "Trusted Networks"
|
||||
|
||||
@property
|
||||
def support_mfa(self) -> bool:
|
||||
|
|
@ -44,27 +43,29 @@ class TrustedNetworksAuthProvider(AuthProvider):
|
|||
"""Return a flow to login."""
|
||||
assert context is not None
|
||||
users = await self.store.async_get_users()
|
||||
available_users = {user.id: user.name
|
||||
available_users = {
|
||||
user.id: user.name
|
||||
for user in users
|
||||
if not user.system_generated and user.is_active}
|
||||
if not user.system_generated and user.is_active
|
||||
}
|
||||
|
||||
return TrustedNetworksLoginFlow(
|
||||
self, cast(str, context.get('ip_address')), available_users)
|
||||
self, cast(str, context.get("ip_address")), available_users
|
||||
)
|
||||
|
||||
async def async_get_or_create_credentials(
|
||||
self, flow_result: Dict[str, str]) -> Credentials:
|
||||
self, flow_result: Dict[str, str]
|
||||
) -> Credentials:
|
||||
"""Get credentials based on the flow result."""
|
||||
user_id = flow_result['user']
|
||||
user_id = flow_result["user"]
|
||||
|
||||
users = await self.store.async_get_users()
|
||||
for user in users:
|
||||
if (not user.system_generated and
|
||||
user.is_active and
|
||||
user.id == user_id):
|
||||
if not user.system_generated and user.is_active and user.id == user_id:
|
||||
for credential in await self.async_credentials():
|
||||
if credential.data['user_id'] == user_id:
|
||||
if credential.data["user_id"] == user_id:
|
||||
return credential
|
||||
cred = self.async_create_credentials({'user_id': user_id})
|
||||
cred = self.async_create_credentials({"user_id": user_id})
|
||||
await self.store.async_link_user(user, cred)
|
||||
return cred
|
||||
|
||||
|
|
@ -72,7 +73,8 @@ class TrustedNetworksAuthProvider(AuthProvider):
|
|||
raise InvalidUserError
|
||||
|
||||
async def async_user_meta_for_credentials(
|
||||
self, credentials: Credentials) -> UserMeta:
|
||||
self, credentials: Credentials
|
||||
) -> UserMeta:
|
||||
"""Return extra user metadata for credentials.
|
||||
|
||||
Trusted network auth provider should never create new user.
|
||||
|
|
@ -86,44 +88,48 @@ class TrustedNetworksAuthProvider(AuthProvider):
|
|||
Raise InvalidAuthError if not.
|
||||
Raise InvalidAuthError if trusted_networks is not configured.
|
||||
"""
|
||||
hass_http = getattr(self.hass, 'http', None) # type: HomeAssistantHTTP
|
||||
hass_http = getattr(self.hass, "http", None) # type: HomeAssistantHTTP
|
||||
|
||||
if not hass_http or not hass_http.trusted_networks:
|
||||
raise InvalidAuthError('trusted_networks is not configured')
|
||||
raise InvalidAuthError("trusted_networks is not configured")
|
||||
|
||||
if not any(ip_address in trusted_network for trusted_network
|
||||
in hass_http.trusted_networks):
|
||||
raise InvalidAuthError('Not in trusted_networks')
|
||||
if not any(
|
||||
ip_address in trusted_network
|
||||
for trusted_network in hass_http.trusted_networks
|
||||
):
|
||||
raise InvalidAuthError("Not in trusted_networks")
|
||||
|
||||
|
||||
class TrustedNetworksLoginFlow(LoginFlow):
|
||||
"""Handler for the login flow."""
|
||||
|
||||
def __init__(self, auth_provider: TrustedNetworksAuthProvider,
|
||||
ip_address: str, available_users: Dict[str, Optional[str]]) \
|
||||
-> None:
|
||||
def __init__(
|
||||
self,
|
||||
auth_provider: TrustedNetworksAuthProvider,
|
||||
ip_address: str,
|
||||
available_users: Dict[str, Optional[str]],
|
||||
) -> None:
|
||||
"""Initialize the login flow."""
|
||||
super().__init__(auth_provider)
|
||||
self._available_users = available_users
|
||||
self._ip_address = ip_address
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: Optional[Dict[str, str]] = None) \
|
||||
-> Dict[str, Any]:
|
||||
self, user_input: Optional[Dict[str, str]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Handle the step of the form."""
|
||||
try:
|
||||
cast(TrustedNetworksAuthProvider, self._auth_provider)\
|
||||
.async_validate_access(self._ip_address)
|
||||
cast(
|
||||
TrustedNetworksAuthProvider, self._auth_provider
|
||||
).async_validate_access(self._ip_address)
|
||||
|
||||
except InvalidAuthError:
|
||||
return self.async_abort(
|
||||
reason='not_whitelisted'
|
||||
)
|
||||
return self.async_abort(reason="not_whitelisted")
|
||||
|
||||
if user_input is not None:
|
||||
return await self.async_finish(user_input)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id='init',
|
||||
data_schema=vol.Schema({'user': vol.In(self._available_users)}),
|
||||
step_id="init",
|
||||
data_schema=vol.Schema({"user": vol.In(self._available_users)}),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -10,4 +10,4 @@ def generate_secret(entropy: int = 32) -> str:
|
|||
|
||||
Event loop friendly.
|
||||
"""
|
||||
return binascii.hexlify(os.urandom(entropy)).decode('ascii')
|
||||
return binascii.hexlify(os.urandom(entropy)).decode("ascii")
|
||||
|
|
|
|||
|
|
@ -10,7 +10,11 @@ from typing import Any, Optional, Dict
|
|||
import voluptuous as vol
|
||||
|
||||
from homeassistant import (
|
||||
core, config as conf_util, config_entries, components as core_components)
|
||||
core,
|
||||
config as conf_util,
|
||||
config_entries,
|
||||
components as core_components,
|
||||
)
|
||||
from homeassistant.components import persistent_notification
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
|
@ -22,16 +26,25 @@ from homeassistant.helpers.signal import async_register_signal_handling
|
|||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ERROR_LOG_FILENAME = 'home-assistant.log'
|
||||
ERROR_LOG_FILENAME = "home-assistant.log"
|
||||
|
||||
# hass.data key for logging information.
|
||||
DATA_LOGGING = 'logging'
|
||||
DATA_LOGGING = "logging"
|
||||
|
||||
FIRST_INIT_COMPONENT = {'system_log', 'recorder', 'mqtt', 'mqtt_eventstream',
|
||||
'logger', 'introduction', 'frontend', 'history'}
|
||||
FIRST_INIT_COMPONENT = {
|
||||
"system_log",
|
||||
"recorder",
|
||||
"mqtt",
|
||||
"mqtt_eventstream",
|
||||
"logger",
|
||||
"introduction",
|
||||
"frontend",
|
||||
"history",
|
||||
}
|
||||
|
||||
|
||||
def from_config_dict(config: Dict[str, Any],
|
||||
def from_config_dict(
|
||||
config: Dict[str, Any],
|
||||
hass: Optional[core.HomeAssistant] = None,
|
||||
config_dir: Optional[str] = None,
|
||||
enable_log: bool = True,
|
||||
|
|
@ -39,8 +52,8 @@ def from_config_dict(config: Dict[str, Any],
|
|||
skip_pip: bool = False,
|
||||
log_rotate_days: Any = None,
|
||||
log_file: Any = None,
|
||||
log_no_color: bool = False) \
|
||||
-> Optional[core.HomeAssistant]:
|
||||
log_no_color: bool = False,
|
||||
) -> Optional[core.HomeAssistant]:
|
||||
"""Try to configure Home Assistant from a configuration dictionary.
|
||||
|
||||
Dynamically loads required components and its dependencies.
|
||||
|
|
@ -51,19 +64,27 @@ def from_config_dict(config: Dict[str, Any],
|
|||
config_dir = os.path.abspath(config_dir)
|
||||
hass.config.config_dir = config_dir
|
||||
if not is_virtual_env():
|
||||
hass.loop.run_until_complete(
|
||||
async_mount_local_lib_path(config_dir))
|
||||
hass.loop.run_until_complete(async_mount_local_lib_path(config_dir))
|
||||
|
||||
# run task
|
||||
hass = hass.loop.run_until_complete(
|
||||
async_from_config_dict(
|
||||
config, hass, config_dir, enable_log, verbose, skip_pip,
|
||||
log_rotate_days, log_file, log_no_color)
|
||||
config,
|
||||
hass,
|
||||
config_dir,
|
||||
enable_log,
|
||||
verbose,
|
||||
skip_pip,
|
||||
log_rotate_days,
|
||||
log_file,
|
||||
log_no_color,
|
||||
)
|
||||
)
|
||||
return hass
|
||||
|
||||
|
||||
async def async_from_config_dict(config: Dict[str, Any],
|
||||
async def async_from_config_dict(
|
||||
config: Dict[str, Any],
|
||||
hass: core.HomeAssistant,
|
||||
config_dir: Optional[str] = None,
|
||||
enable_log: bool = True,
|
||||
|
|
@ -71,8 +92,8 @@ async def async_from_config_dict(config: Dict[str, Any],
|
|||
skip_pip: bool = False,
|
||||
log_rotate_days: Any = None,
|
||||
log_file: Any = None,
|
||||
log_no_color: bool = False) \
|
||||
-> Optional[core.HomeAssistant]:
|
||||
log_no_color: bool = False,
|
||||
) -> Optional[core.HomeAssistant]:
|
||||
"""Try to configure Home Assistant from a configuration dictionary.
|
||||
|
||||
Dynamically loads required components and its dependencies.
|
||||
|
|
@ -81,40 +102,41 @@ async def async_from_config_dict(config: Dict[str, Any],
|
|||
start = time()
|
||||
|
||||
if enable_log:
|
||||
async_enable_logging(hass, verbose, log_rotate_days, log_file,
|
||||
log_no_color)
|
||||
async_enable_logging(hass, verbose, log_rotate_days, log_file, log_no_color)
|
||||
|
||||
core_config = config.get(core.DOMAIN, {})
|
||||
has_api_password = bool((config.get('http') or {}).get('api_password'))
|
||||
has_trusted_networks = bool((config.get('http') or {})
|
||||
.get('trusted_networks'))
|
||||
has_api_password = bool((config.get("http") or {}).get("api_password"))
|
||||
has_trusted_networks = bool((config.get("http") or {}).get("trusted_networks"))
|
||||
|
||||
try:
|
||||
await conf_util.async_process_ha_core_config(
|
||||
hass, core_config, has_api_password, has_trusted_networks)
|
||||
hass, core_config, has_api_password, has_trusted_networks
|
||||
)
|
||||
except vol.Invalid as config_err:
|
||||
conf_util.async_log_exception(
|
||||
config_err, 'homeassistant', core_config, hass)
|
||||
conf_util.async_log_exception(config_err, "homeassistant", core_config, hass)
|
||||
return None
|
||||
except HomeAssistantError:
|
||||
_LOGGER.error("Home Assistant core failed to initialize. "
|
||||
"Further initialization aborted")
|
||||
_LOGGER.error(
|
||||
"Home Assistant core failed to initialize. "
|
||||
"Further initialization aborted"
|
||||
)
|
||||
return None
|
||||
|
||||
await hass.async_add_executor_job(
|
||||
conf_util.process_ha_config_upgrade, hass)
|
||||
await hass.async_add_executor_job(conf_util.process_ha_config_upgrade, hass)
|
||||
|
||||
hass.config.skip_pip = skip_pip
|
||||
if skip_pip:
|
||||
_LOGGER.warning("Skipping pip installation of required modules. "
|
||||
"This may cause issues")
|
||||
_LOGGER.warning(
|
||||
"Skipping pip installation of required modules. " "This may cause issues"
|
||||
)
|
||||
|
||||
# Make a copy because we are mutating it.
|
||||
config = OrderedDict(config)
|
||||
|
||||
# Merge packages
|
||||
conf_util.merge_packages_config(
|
||||
hass, config, core_config.get(conf_util.CONF_PACKAGES, {}))
|
||||
hass, config, core_config.get(conf_util.CONF_PACKAGES, {})
|
||||
)
|
||||
|
||||
# Ensure we have no None values after merge
|
||||
for key, value in config.items():
|
||||
|
|
@ -125,15 +147,16 @@ async def async_from_config_dict(config: Dict[str, Any],
|
|||
await hass.config_entries.async_load()
|
||||
|
||||
# Filter out the repeating and common config section [homeassistant]
|
||||
components = set(key.split(' ')[0] for key in config.keys()
|
||||
if key != core.DOMAIN)
|
||||
components = set(key.split(" ")[0] for key in config.keys() if key != core.DOMAIN)
|
||||
components.update(hass.config_entries.async_domains())
|
||||
|
||||
# setup components
|
||||
res = await core_components.async_setup(hass, config)
|
||||
if not res:
|
||||
_LOGGER.error("Home Assistant core failed to initialize. "
|
||||
"Further initialization aborted")
|
||||
_LOGGER.error(
|
||||
"Home Assistant core failed to initialize. "
|
||||
"Further initialization aborted"
|
||||
)
|
||||
return hass
|
||||
|
||||
await persistent_notification.async_setup(hass, config)
|
||||
|
|
@ -163,14 +186,15 @@ async def async_from_config_dict(config: Dict[str, Any],
|
|||
return hass
|
||||
|
||||
|
||||
def from_config_file(config_path: str,
|
||||
def from_config_file(
|
||||
config_path: str,
|
||||
hass: Optional[core.HomeAssistant] = None,
|
||||
verbose: bool = False,
|
||||
skip_pip: bool = True,
|
||||
log_rotate_days: Any = None,
|
||||
log_file: Any = None,
|
||||
log_no_color: bool = False)\
|
||||
-> Optional[core.HomeAssistant]:
|
||||
log_no_color: bool = False,
|
||||
) -> Optional[core.HomeAssistant]:
|
||||
"""Read the configuration file and try to start all the functionality.
|
||||
|
||||
Will add functionality to 'hass' parameter if given,
|
||||
|
|
@ -182,21 +206,28 @@ def from_config_file(config_path: str,
|
|||
# run task
|
||||
hass = hass.loop.run_until_complete(
|
||||
async_from_config_file(
|
||||
config_path, hass, verbose, skip_pip,
|
||||
log_rotate_days, log_file, log_no_color)
|
||||
config_path,
|
||||
hass,
|
||||
verbose,
|
||||
skip_pip,
|
||||
log_rotate_days,
|
||||
log_file,
|
||||
log_no_color,
|
||||
)
|
||||
)
|
||||
|
||||
return hass
|
||||
|
||||
|
||||
async def async_from_config_file(config_path: str,
|
||||
async def async_from_config_file(
|
||||
config_path: str,
|
||||
hass: core.HomeAssistant,
|
||||
verbose: bool = False,
|
||||
skip_pip: bool = True,
|
||||
log_rotate_days: Any = None,
|
||||
log_file: Any = None,
|
||||
log_no_color: bool = False)\
|
||||
-> Optional[core.HomeAssistant]:
|
||||
log_no_color: bool = False,
|
||||
) -> Optional[core.HomeAssistant]:
|
||||
"""Read the configuration file and try to start all the functionality.
|
||||
|
||||
Will add functionality to 'hass' parameter.
|
||||
|
|
@ -209,12 +240,12 @@ async def async_from_config_file(config_path: str,
|
|||
if not is_virtual_env():
|
||||
await async_mount_local_lib_path(config_dir)
|
||||
|
||||
async_enable_logging(hass, verbose, log_rotate_days, log_file,
|
||||
log_no_color)
|
||||
async_enable_logging(hass, verbose, log_rotate_days, log_file, log_no_color)
|
||||
|
||||
try:
|
||||
config_dict = await hass.async_add_executor_job(
|
||||
conf_util.load_yaml_config_file, config_path)
|
||||
conf_util.load_yaml_config_file, config_path
|
||||
)
|
||||
except HomeAssistantError as err:
|
||||
_LOGGER.error("Error loading %s: %s", config_path, err)
|
||||
return None
|
||||
|
|
@ -222,43 +253,48 @@ async def async_from_config_file(config_path: str,
|
|||
clear_secret_cache()
|
||||
|
||||
return await async_from_config_dict(
|
||||
config_dict, hass, enable_log=False, skip_pip=skip_pip)
|
||||
config_dict, hass, enable_log=False, skip_pip=skip_pip
|
||||
)
|
||||
|
||||
|
||||
@core.callback
|
||||
def async_enable_logging(hass: core.HomeAssistant,
|
||||
def async_enable_logging(
|
||||
hass: core.HomeAssistant,
|
||||
verbose: bool = False,
|
||||
log_rotate_days: Optional[int] = None,
|
||||
log_file: Optional[str] = None,
|
||||
log_no_color: bool = False) -> None:
|
||||
log_no_color: bool = False,
|
||||
) -> None:
|
||||
"""Set up the logging.
|
||||
|
||||
This method must be run in the event loop.
|
||||
"""
|
||||
fmt = ("%(asctime)s %(levelname)s (%(threadName)s) "
|
||||
"[%(name)s] %(message)s")
|
||||
datefmt = '%Y-%m-%d %H:%M:%S'
|
||||
fmt = "%(asctime)s %(levelname)s (%(threadName)s) " "[%(name)s] %(message)s"
|
||||
datefmt = "%Y-%m-%d %H:%M:%S"
|
||||
|
||||
if not log_no_color:
|
||||
try:
|
||||
from colorlog import ColoredFormatter
|
||||
|
||||
# basicConfig must be called after importing colorlog in order to
|
||||
# ensure that the handlers it sets up wraps the correct streams.
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
colorfmt = "%(log_color)s{}%(reset)s".format(fmt)
|
||||
logging.getLogger().handlers[0].setFormatter(ColoredFormatter(
|
||||
logging.getLogger().handlers[0].setFormatter(
|
||||
ColoredFormatter(
|
||||
colorfmt,
|
||||
datefmt=datefmt,
|
||||
reset=True,
|
||||
log_colors={
|
||||
'DEBUG': 'cyan',
|
||||
'INFO': 'green',
|
||||
'WARNING': 'yellow',
|
||||
'ERROR': 'red',
|
||||
'CRITICAL': 'red',
|
||||
}
|
||||
))
|
||||
"DEBUG": "cyan",
|
||||
"INFO": "green",
|
||||
"WARNING": "yellow",
|
||||
"ERROR": "red",
|
||||
"CRITICAL": "red",
|
||||
},
|
||||
)
|
||||
)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
|
@ -267,9 +303,9 @@ def async_enable_logging(hass: core.HomeAssistant,
|
|||
logging.basicConfig(format=fmt, datefmt=datefmt, level=logging.INFO)
|
||||
|
||||
# Suppress overly verbose logs from libraries that aren't helpful
|
||||
logging.getLogger('requests').setLevel(logging.WARNING)
|
||||
logging.getLogger('urllib3').setLevel(logging.WARNING)
|
||||
logging.getLogger('aiohttp.access').setLevel(logging.WARNING)
|
||||
logging.getLogger("requests").setLevel(logging.WARNING)
|
||||
logging.getLogger("urllib3").setLevel(logging.WARNING)
|
||||
logging.getLogger("aiohttp.access").setLevel(logging.WARNING)
|
||||
|
||||
# Log errors to a file if we have write access to file or config dir
|
||||
if log_file is None:
|
||||
|
|
@ -282,16 +318,16 @@ def async_enable_logging(hass: core.HomeAssistant,
|
|||
|
||||
# Check if we can write to the error log if it exists or that
|
||||
# we can create files in the containing directory if not.
|
||||
if (err_path_exists and os.access(err_log_path, os.W_OK)) or \
|
||||
(not err_path_exists and os.access(err_dir, os.W_OK)):
|
||||
if (err_path_exists and os.access(err_log_path, os.W_OK)) or (
|
||||
not err_path_exists and os.access(err_dir, os.W_OK)
|
||||
):
|
||||
|
||||
if log_rotate_days:
|
||||
err_handler = logging.handlers.TimedRotatingFileHandler(
|
||||
err_log_path, when='midnight',
|
||||
backupCount=log_rotate_days) # type: logging.FileHandler
|
||||
err_log_path, when="midnight", backupCount=log_rotate_days
|
||||
) # type: logging.FileHandler
|
||||
else:
|
||||
err_handler = logging.FileHandler(
|
||||
err_log_path, mode='w', delay=True)
|
||||
err_handler = logging.FileHandler(err_log_path, mode="w", delay=True)
|
||||
|
||||
err_handler.setLevel(logging.INFO if verbose else logging.WARNING)
|
||||
err_handler.setFormatter(logging.Formatter(fmt, datefmt=datefmt))
|
||||
|
|
@ -300,21 +336,19 @@ def async_enable_logging(hass: core.HomeAssistant,
|
|||
|
||||
async def async_stop_async_handler(_: Any) -> None:
|
||||
"""Cleanup async handler."""
|
||||
logging.getLogger('').removeHandler(async_handler) # type: ignore
|
||||
logging.getLogger("").removeHandler(async_handler) # type: ignore
|
||||
await async_handler.async_close(blocking=True)
|
||||
|
||||
hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_CLOSE, async_stop_async_handler)
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_CLOSE, async_stop_async_handler)
|
||||
|
||||
logger = logging.getLogger('')
|
||||
logger = logging.getLogger("")
|
||||
logger.addHandler(async_handler) # type: ignore
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
# Save the log file location for access by other components.
|
||||
hass.data[DATA_LOGGING] = err_log_path
|
||||
else:
|
||||
_LOGGER.error(
|
||||
"Unable to set up error log %s (access denied)", err_log_path)
|
||||
_LOGGER.error("Unable to set up error log %s (access denied)", err_log_path)
|
||||
|
||||
|
||||
async def async_mount_local_lib_path(config_dir: str) -> str:
|
||||
|
|
@ -322,7 +356,7 @@ async def async_mount_local_lib_path(config_dir: str) -> str:
|
|||
|
||||
This function is a coroutine.
|
||||
"""
|
||||
deps_dir = os.path.join(config_dir, 'deps')
|
||||
deps_dir = os.path.join(config_dir, "deps")
|
||||
lib_dir = await async_get_user_site(deps_dir)
|
||||
if lib_dir not in sys.path:
|
||||
sys.path.insert(0, lib_dir)
|
||||
|
|
|
|||
|
|
@ -18,14 +18,19 @@ from homeassistant.exceptions import HomeAssistantError
|
|||
from homeassistant.helpers.service import extract_entity_ids
|
||||
from homeassistant.helpers import intent
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE,
|
||||
SERVICE_HOMEASSISTANT_STOP, SERVICE_HOMEASSISTANT_RESTART,
|
||||
RESTART_EXIT_CODE)
|
||||
ATTR_ENTITY_ID,
|
||||
SERVICE_TURN_ON,
|
||||
SERVICE_TURN_OFF,
|
||||
SERVICE_TOGGLE,
|
||||
SERVICE_HOMEASSISTANT_STOP,
|
||||
SERVICE_HOMEASSISTANT_RESTART,
|
||||
RESTART_EXIT_CODE,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SERVICE_RELOAD_CORE_CONFIG = 'reload_core_config'
|
||||
SERVICE_CHECK_CONFIG = 'check_config'
|
||||
SERVICE_RELOAD_CORE_CONFIG = "reload_core_config"
|
||||
SERVICE_CHECK_CONFIG = "check_config"
|
||||
|
||||
|
||||
def is_on(hass, entity_id=None):
|
||||
|
|
@ -45,11 +50,10 @@ def is_on(hass, entity_id=None):
|
|||
component = getattr(hass.components, domain)
|
||||
|
||||
except ImportError:
|
||||
_LOGGER.error('Failed to call %s.is_on: component not found',
|
||||
domain)
|
||||
_LOGGER.error("Failed to call %s.is_on: component not found", domain)
|
||||
continue
|
||||
|
||||
if not hasattr(component, 'is_on'):
|
||||
if not hasattr(component, "is_on"):
|
||||
_LOGGER.warning("Component %s has no is_on method.", domain)
|
||||
continue
|
||||
|
||||
|
|
@ -112,6 +116,7 @@ def async_reload_core_config(hass):
|
|||
@asyncio.coroutine
|
||||
def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]:
|
||||
"""Set up general services related to Home Assistant."""
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_handle_turn_service(service):
|
||||
"""Handle calls to homeassistant.turn_on/off."""
|
||||
|
|
@ -120,13 +125,14 @@ def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]:
|
|||
# Generic turn on/off method requires entity id
|
||||
if not entity_ids:
|
||||
_LOGGER.error(
|
||||
"homeassistant/%s cannot be called without entity_id",
|
||||
service.service)
|
||||
"homeassistant/%s cannot be called without entity_id", service.service
|
||||
)
|
||||
return
|
||||
|
||||
# Group entity_ids by domain. groupby requires sorted data.
|
||||
by_domain = it.groupby(sorted(entity_ids),
|
||||
lambda item: ha.split_entity_id(item)[0])
|
||||
by_domain = it.groupby(
|
||||
sorted(entity_ids), lambda item: ha.split_entity_id(item)[0]
|
||||
)
|
||||
|
||||
tasks = []
|
||||
|
||||
|
|
@ -145,24 +151,30 @@ def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]:
|
|||
# ent_ids is a generator, convert it to a list.
|
||||
data[ATTR_ENTITY_ID] = list(ent_ids)
|
||||
|
||||
tasks.append(hass.services.async_call(
|
||||
domain, service.service, data, blocking))
|
||||
tasks.append(
|
||||
hass.services.async_call(domain, service.service, data, blocking)
|
||||
)
|
||||
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
hass.services.async_register(
|
||||
ha.DOMAIN, SERVICE_TURN_OFF, async_handle_turn_service)
|
||||
hass.services.async_register(
|
||||
ha.DOMAIN, SERVICE_TURN_ON, async_handle_turn_service)
|
||||
hass.services.async_register(
|
||||
ha.DOMAIN, SERVICE_TOGGLE, async_handle_turn_service)
|
||||
hass.helpers.intent.async_register(intent.ServiceIntentHandler(
|
||||
intent.INTENT_TURN_ON, ha.DOMAIN, SERVICE_TURN_ON, "Turned {} on"))
|
||||
hass.helpers.intent.async_register(intent.ServiceIntentHandler(
|
||||
intent.INTENT_TURN_OFF, ha.DOMAIN, SERVICE_TURN_OFF,
|
||||
"Turned {} off"))
|
||||
hass.helpers.intent.async_register(intent.ServiceIntentHandler(
|
||||
intent.INTENT_TOGGLE, ha.DOMAIN, SERVICE_TOGGLE, "Toggled {}"))
|
||||
hass.services.async_register(ha.DOMAIN, SERVICE_TURN_OFF, async_handle_turn_service)
|
||||
hass.services.async_register(ha.DOMAIN, SERVICE_TURN_ON, async_handle_turn_service)
|
||||
hass.services.async_register(ha.DOMAIN, SERVICE_TOGGLE, async_handle_turn_service)
|
||||
hass.helpers.intent.async_register(
|
||||
intent.ServiceIntentHandler(
|
||||
intent.INTENT_TURN_ON, ha.DOMAIN, SERVICE_TURN_ON, "Turned {} on"
|
||||
)
|
||||
)
|
||||
hass.helpers.intent.async_register(
|
||||
intent.ServiceIntentHandler(
|
||||
intent.INTENT_TURN_OFF, ha.DOMAIN, SERVICE_TURN_OFF, "Turned {} off"
|
||||
)
|
||||
)
|
||||
hass.helpers.intent.async_register(
|
||||
intent.ServiceIntentHandler(
|
||||
intent.INTENT_TOGGLE, ha.DOMAIN, SERVICE_TOGGLE, "Toggled {}"
|
||||
)
|
||||
)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_handle_core_service(call):
|
||||
|
|
@ -180,18 +192,23 @@ def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]:
|
|||
_LOGGER.error(errors)
|
||||
hass.components.persistent_notification.async_create(
|
||||
"Config error. See dev-info panel for details.",
|
||||
"Config validating", "{0}.check_config".format(ha.DOMAIN))
|
||||
"Config validating",
|
||||
"{0}.check_config".format(ha.DOMAIN),
|
||||
)
|
||||
return
|
||||
|
||||
if call.service == SERVICE_HOMEASSISTANT_RESTART:
|
||||
hass.async_create_task(hass.async_stop(RESTART_EXIT_CODE))
|
||||
|
||||
hass.services.async_register(
|
||||
ha.DOMAIN, SERVICE_HOMEASSISTANT_STOP, async_handle_core_service)
|
||||
ha.DOMAIN, SERVICE_HOMEASSISTANT_STOP, async_handle_core_service
|
||||
)
|
||||
hass.services.async_register(
|
||||
ha.DOMAIN, SERVICE_HOMEASSISTANT_RESTART, async_handle_core_service)
|
||||
ha.DOMAIN, SERVICE_HOMEASSISTANT_RESTART, async_handle_core_service
|
||||
)
|
||||
hass.services.async_register(
|
||||
ha.DOMAIN, SERVICE_CHECK_CONFIG, async_handle_core_service)
|
||||
ha.DOMAIN, SERVICE_CHECK_CONFIG, async_handle_core_service
|
||||
)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_handle_reload_config(call):
|
||||
|
|
@ -203,9 +220,11 @@ def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]:
|
|||
return
|
||||
|
||||
yield from conf_util.async_process_ha_core_config(
|
||||
hass, conf.get(ha.DOMAIN) or {})
|
||||
hass, conf.get(ha.DOMAIN) or {}
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG, async_handle_reload_config)
|
||||
ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG, async_handle_reload_config
|
||||
)
|
||||
|
||||
return True
|
||||
|
|
|
|||
|
|
@ -12,89 +12,109 @@ from requests.exceptions import HTTPError, ConnectTimeout
|
|||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
ATTR_ATTRIBUTION, ATTR_DATE, ATTR_TIME, ATTR_ENTITY_ID, CONF_USERNAME,
|
||||
CONF_PASSWORD, CONF_EXCLUDE, CONF_NAME, CONF_LIGHTS,
|
||||
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START)
|
||||
ATTR_ATTRIBUTION,
|
||||
ATTR_DATE,
|
||||
ATTR_TIME,
|
||||
ATTR_ENTITY_ID,
|
||||
CONF_USERNAME,
|
||||
CONF_PASSWORD,
|
||||
CONF_EXCLUDE,
|
||||
CONF_NAME,
|
||||
CONF_LIGHTS,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
EVENT_HOMEASSISTANT_START,
|
||||
)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
REQUIREMENTS = ['abodepy==0.13.1']
|
||||
REQUIREMENTS = ["abodepy==0.13.1"]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_ATTRIBUTION = "Data provided by goabode.com"
|
||||
CONF_POLLING = 'polling'
|
||||
CONF_POLLING = "polling"
|
||||
|
||||
DOMAIN = 'abode'
|
||||
DEFAULT_CACHEDB = './abodepy_cache.pickle'
|
||||
DOMAIN = "abode"
|
||||
DEFAULT_CACHEDB = "./abodepy_cache.pickle"
|
||||
|
||||
NOTIFICATION_ID = 'abode_notification'
|
||||
NOTIFICATION_TITLE = 'Abode Security Setup'
|
||||
NOTIFICATION_ID = "abode_notification"
|
||||
NOTIFICATION_TITLE = "Abode Security Setup"
|
||||
|
||||
EVENT_ABODE_ALARM = 'abode_alarm'
|
||||
EVENT_ABODE_ALARM_END = 'abode_alarm_end'
|
||||
EVENT_ABODE_AUTOMATION = 'abode_automation'
|
||||
EVENT_ABODE_FAULT = 'abode_panel_fault'
|
||||
EVENT_ABODE_RESTORE = 'abode_panel_restore'
|
||||
EVENT_ABODE_ALARM = "abode_alarm"
|
||||
EVENT_ABODE_ALARM_END = "abode_alarm_end"
|
||||
EVENT_ABODE_AUTOMATION = "abode_automation"
|
||||
EVENT_ABODE_FAULT = "abode_panel_fault"
|
||||
EVENT_ABODE_RESTORE = "abode_panel_restore"
|
||||
|
||||
SERVICE_SETTINGS = 'change_setting'
|
||||
SERVICE_CAPTURE_IMAGE = 'capture_image'
|
||||
SERVICE_TRIGGER = 'trigger_quick_action'
|
||||
SERVICE_SETTINGS = "change_setting"
|
||||
SERVICE_CAPTURE_IMAGE = "capture_image"
|
||||
SERVICE_TRIGGER = "trigger_quick_action"
|
||||
|
||||
ATTR_DEVICE_ID = 'device_id'
|
||||
ATTR_DEVICE_NAME = 'device_name'
|
||||
ATTR_DEVICE_TYPE = 'device_type'
|
||||
ATTR_EVENT_CODE = 'event_code'
|
||||
ATTR_EVENT_NAME = 'event_name'
|
||||
ATTR_EVENT_TYPE = 'event_type'
|
||||
ATTR_EVENT_UTC = 'event_utc'
|
||||
ATTR_SETTING = 'setting'
|
||||
ATTR_USER_NAME = 'user_name'
|
||||
ATTR_VALUE = 'value'
|
||||
ATTR_DEVICE_ID = "device_id"
|
||||
ATTR_DEVICE_NAME = "device_name"
|
||||
ATTR_DEVICE_TYPE = "device_type"
|
||||
ATTR_EVENT_CODE = "event_code"
|
||||
ATTR_EVENT_NAME = "event_name"
|
||||
ATTR_EVENT_TYPE = "event_type"
|
||||
ATTR_EVENT_UTC = "event_utc"
|
||||
ATTR_SETTING = "setting"
|
||||
ATTR_USER_NAME = "user_name"
|
||||
ATTR_VALUE = "value"
|
||||
|
||||
ABODE_DEVICE_ID_LIST_SCHEMA = vol.Schema([str])
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
DOMAIN: vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_POLLING, default=False): cv.boolean,
|
||||
vol.Optional(CONF_EXCLUDE, default=[]): ABODE_DEVICE_ID_LIST_SCHEMA,
|
||||
vol.Optional(CONF_LIGHTS, default=[]): ABODE_DEVICE_ID_LIST_SCHEMA
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
vol.Optional(CONF_LIGHTS, default=[]): ABODE_DEVICE_ID_LIST_SCHEMA,
|
||||
}
|
||||
)
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
CHANGE_SETTING_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_SETTING): cv.string,
|
||||
vol.Required(ATTR_VALUE): cv.string
|
||||
})
|
||||
CHANGE_SETTING_SCHEMA = vol.Schema(
|
||||
{vol.Required(ATTR_SETTING): cv.string, vol.Required(ATTR_VALUE): cv.string}
|
||||
)
|
||||
|
||||
CAPTURE_IMAGE_SCHEMA = vol.Schema({
|
||||
ATTR_ENTITY_ID: cv.entity_ids,
|
||||
})
|
||||
CAPTURE_IMAGE_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids})
|
||||
|
||||
TRIGGER_SCHEMA = vol.Schema({
|
||||
ATTR_ENTITY_ID: cv.entity_ids,
|
||||
})
|
||||
TRIGGER_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids})
|
||||
|
||||
ABODE_PLATFORMS = [
|
||||
'alarm_control_panel', 'binary_sensor', 'lock', 'switch', 'cover',
|
||||
'camera', 'light', 'sensor'
|
||||
"alarm_control_panel",
|
||||
"binary_sensor",
|
||||
"lock",
|
||||
"switch",
|
||||
"cover",
|
||||
"camera",
|
||||
"light",
|
||||
"sensor",
|
||||
]
|
||||
|
||||
|
||||
class AbodeSystem:
|
||||
"""Abode System class."""
|
||||
|
||||
def __init__(self, username, password, cache,
|
||||
name, polling, exclude, lights):
|
||||
def __init__(self, username, password, cache, name, polling, exclude, lights):
|
||||
"""Initialize the system."""
|
||||
import abodepy
|
||||
|
||||
self.abode = abodepy.Abode(
|
||||
username, password, auto_login=True, get_devices=True,
|
||||
get_automations=True, cache_path=cache)
|
||||
username,
|
||||
password,
|
||||
auto_login=True,
|
||||
get_devices=True,
|
||||
get_automations=True,
|
||||
cache_path=cache,
|
||||
)
|
||||
self.name = name
|
||||
self.polling = polling
|
||||
self.exclude = exclude
|
||||
|
|
@ -113,9 +133,9 @@ class AbodeSystem:
|
|||
"""Check if a switch device is configured as a light."""
|
||||
import abodepy.helpers.constants as CONST
|
||||
|
||||
return (device.generic_type == CONST.TYPE_LIGHT or
|
||||
(device.generic_type == CONST.TYPE_SWITCH and
|
||||
device.device_id in self.lights))
|
||||
return device.generic_type == CONST.TYPE_LIGHT or (
|
||||
device.generic_type == CONST.TYPE_SWITCH and device.device_id in self.lights
|
||||
)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
|
|
@ -133,16 +153,18 @@ def setup(hass, config):
|
|||
try:
|
||||
cache = hass.config.path(DEFAULT_CACHEDB)
|
||||
hass.data[DOMAIN] = AbodeSystem(
|
||||
username, password, cache, name, polling, exclude, lights)
|
||||
username, password, cache, name, polling, exclude, lights
|
||||
)
|
||||
except (AbodeException, ConnectTimeout, HTTPError) as ex:
|
||||
_LOGGER.error("Unable to connect to Abode: %s", str(ex))
|
||||
|
||||
hass.components.persistent_notification.create(
|
||||
'Error: {}<br />'
|
||||
'You will need to restart hass after fixing.'
|
||||
''.format(ex),
|
||||
"Error: {}<br />"
|
||||
"You will need to restart hass after fixing."
|
||||
"".format(ex),
|
||||
title=NOTIFICATION_TITLE,
|
||||
notification_id=NOTIFICATION_ID)
|
||||
notification_id=NOTIFICATION_ID,
|
||||
)
|
||||
return False
|
||||
|
||||
setup_hass_services(hass)
|
||||
|
|
@ -173,8 +195,11 @@ def setup_hass_services(hass):
|
|||
"""Capture a new image."""
|
||||
entity_ids = call.data.get(ATTR_ENTITY_ID)
|
||||
|
||||
target_devices = [device for device in hass.data[DOMAIN].devices
|
||||
if device.entity_id in entity_ids]
|
||||
target_devices = [
|
||||
device
|
||||
for device in hass.data[DOMAIN].devices
|
||||
if device.entity_id in entity_ids
|
||||
]
|
||||
|
||||
for device in target_devices:
|
||||
device.capture()
|
||||
|
|
@ -183,27 +208,31 @@ def setup_hass_services(hass):
|
|||
"""Trigger a quick action."""
|
||||
entity_ids = call.data.get(ATTR_ENTITY_ID, None)
|
||||
|
||||
target_devices = [device for device in hass.data[DOMAIN].devices
|
||||
if device.entity_id in entity_ids]
|
||||
target_devices = [
|
||||
device
|
||||
for device in hass.data[DOMAIN].devices
|
||||
if device.entity_id in entity_ids
|
||||
]
|
||||
|
||||
for device in target_devices:
|
||||
device.trigger()
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_SETTINGS, change_setting,
|
||||
schema=CHANGE_SETTING_SCHEMA)
|
||||
DOMAIN, SERVICE_SETTINGS, change_setting, schema=CHANGE_SETTING_SCHEMA
|
||||
)
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_CAPTURE_IMAGE, capture_image,
|
||||
schema=CAPTURE_IMAGE_SCHEMA)
|
||||
DOMAIN, SERVICE_CAPTURE_IMAGE, capture_image, schema=CAPTURE_IMAGE_SCHEMA
|
||||
)
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_TRIGGER, trigger_quick_action,
|
||||
schema=TRIGGER_SCHEMA)
|
||||
DOMAIN, SERVICE_TRIGGER, trigger_quick_action, schema=TRIGGER_SCHEMA
|
||||
)
|
||||
|
||||
|
||||
def setup_hass_events(hass):
|
||||
"""Home Assistant start and stop callbacks."""
|
||||
|
||||
def startup(event):
|
||||
"""Listen for push events."""
|
||||
hass.data[DOMAIN].abode.events.start()
|
||||
|
|
@ -229,28 +258,32 @@ def setup_abode_events(hass):
|
|||
def event_callback(event, event_json):
|
||||
"""Handle an event callback from Abode."""
|
||||
data = {
|
||||
ATTR_DEVICE_ID: event_json.get(ATTR_DEVICE_ID, ''),
|
||||
ATTR_DEVICE_NAME: event_json.get(ATTR_DEVICE_NAME, ''),
|
||||
ATTR_DEVICE_TYPE: event_json.get(ATTR_DEVICE_TYPE, ''),
|
||||
ATTR_EVENT_CODE: event_json.get(ATTR_EVENT_CODE, ''),
|
||||
ATTR_EVENT_NAME: event_json.get(ATTR_EVENT_NAME, ''),
|
||||
ATTR_EVENT_TYPE: event_json.get(ATTR_EVENT_TYPE, ''),
|
||||
ATTR_EVENT_UTC: event_json.get(ATTR_EVENT_UTC, ''),
|
||||
ATTR_USER_NAME: event_json.get(ATTR_USER_NAME, ''),
|
||||
ATTR_DATE: event_json.get(ATTR_DATE, ''),
|
||||
ATTR_TIME: event_json.get(ATTR_TIME, ''),
|
||||
ATTR_DEVICE_ID: event_json.get(ATTR_DEVICE_ID, ""),
|
||||
ATTR_DEVICE_NAME: event_json.get(ATTR_DEVICE_NAME, ""),
|
||||
ATTR_DEVICE_TYPE: event_json.get(ATTR_DEVICE_TYPE, ""),
|
||||
ATTR_EVENT_CODE: event_json.get(ATTR_EVENT_CODE, ""),
|
||||
ATTR_EVENT_NAME: event_json.get(ATTR_EVENT_NAME, ""),
|
||||
ATTR_EVENT_TYPE: event_json.get(ATTR_EVENT_TYPE, ""),
|
||||
ATTR_EVENT_UTC: event_json.get(ATTR_EVENT_UTC, ""),
|
||||
ATTR_USER_NAME: event_json.get(ATTR_USER_NAME, ""),
|
||||
ATTR_DATE: event_json.get(ATTR_DATE, ""),
|
||||
ATTR_TIME: event_json.get(ATTR_TIME, ""),
|
||||
}
|
||||
|
||||
hass.bus.fire(event, data)
|
||||
|
||||
events = [TIMELINE.ALARM_GROUP, TIMELINE.ALARM_END_GROUP,
|
||||
TIMELINE.PANEL_FAULT_GROUP, TIMELINE.PANEL_RESTORE_GROUP,
|
||||
TIMELINE.AUTOMATION_GROUP]
|
||||
events = [
|
||||
TIMELINE.ALARM_GROUP,
|
||||
TIMELINE.ALARM_END_GROUP,
|
||||
TIMELINE.PANEL_FAULT_GROUP,
|
||||
TIMELINE.PANEL_RESTORE_GROUP,
|
||||
TIMELINE.AUTOMATION_GROUP,
|
||||
]
|
||||
|
||||
for event in events:
|
||||
hass.data[DOMAIN].abode.events.add_event_callback(
|
||||
event,
|
||||
partial(event_callback, event))
|
||||
event, partial(event_callback, event)
|
||||
)
|
||||
|
||||
|
||||
class AbodeDevice(Entity):
|
||||
|
|
@ -266,7 +299,8 @@ class AbodeDevice(Entity):
|
|||
"""Subscribe Abode events."""
|
||||
self.hass.async_add_job(
|
||||
self._data.abode.events.add_device_callback,
|
||||
self._device.device_id, self._update_callback
|
||||
self._device.device_id,
|
||||
self._update_callback,
|
||||
)
|
||||
|
||||
@property
|
||||
|
|
@ -288,10 +322,10 @@ class AbodeDevice(Entity):
|
|||
"""Return the state attributes."""
|
||||
return {
|
||||
ATTR_ATTRIBUTION: CONF_ATTRIBUTION,
|
||||
'device_id': self._device.device_id,
|
||||
'battery_low': self._device.battery_low,
|
||||
'no_response': self._device.no_response,
|
||||
'device_type': self._device.type
|
||||
"device_id": self._device.device_id,
|
||||
"battery_low": self._device.battery_low,
|
||||
"no_response": self._device.no_response,
|
||||
"device_type": self._device.type,
|
||||
}
|
||||
|
||||
def _update_callback(self, device):
|
||||
|
|
@ -314,7 +348,8 @@ class AbodeAutomation(Entity):
|
|||
if self._event:
|
||||
self.hass.async_add_job(
|
||||
self._data.abode.events.add_event_callback,
|
||||
self._event, self._update_callback
|
||||
self._event,
|
||||
self._update_callback,
|
||||
)
|
||||
|
||||
@property
|
||||
|
|
@ -336,9 +371,9 @@ class AbodeAutomation(Entity):
|
|||
"""Return the state attributes."""
|
||||
return {
|
||||
ATTR_ATTRIBUTION: CONF_ATTRIBUTION,
|
||||
'automation_id': self._automation.automation_id,
|
||||
'type': self._automation.type,
|
||||
'sub_type': self._automation.sub_type
|
||||
"automation_id": self._automation.automation_id,
|
||||
"type": self._automation.type,
|
||||
"sub_type": self._automation.sub_type,
|
||||
}
|
||||
|
||||
def _update_callback(self, device):
|
||||
|
|
|
|||
|
|
@ -10,51 +10,62 @@ import logging
|
|||
import ctypes
|
||||
from collections import namedtuple
|
||||
import voluptuous as vol
|
||||
from homeassistant.const import CONF_DEVICE, CONF_PORT, CONF_IP_ADDRESS, \
|
||||
EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.const import (
|
||||
CONF_DEVICE,
|
||||
CONF_PORT,
|
||||
CONF_IP_ADDRESS,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pyads==2.2.6']
|
||||
REQUIREMENTS = ["pyads==2.2.6"]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DATA_ADS = 'data_ads'
|
||||
DATA_ADS = "data_ads"
|
||||
|
||||
# Supported Types
|
||||
ADSTYPE_INT = 'int'
|
||||
ADSTYPE_UINT = 'uint'
|
||||
ADSTYPE_BYTE = 'byte'
|
||||
ADSTYPE_BOOL = 'bool'
|
||||
ADSTYPE_INT = "int"
|
||||
ADSTYPE_UINT = "uint"
|
||||
ADSTYPE_BYTE = "byte"
|
||||
ADSTYPE_BOOL = "bool"
|
||||
|
||||
DOMAIN = 'ads'
|
||||
DOMAIN = "ads"
|
||||
|
||||
CONF_ADS_VAR = 'adsvar'
|
||||
CONF_ADS_VAR_BRIGHTNESS = 'adsvar_brightness'
|
||||
CONF_ADS_TYPE = 'adstype'
|
||||
CONF_ADS_FACTOR = 'factor'
|
||||
CONF_ADS_VALUE = 'value'
|
||||
CONF_ADS_VAR = "adsvar"
|
||||
CONF_ADS_VAR_BRIGHTNESS = "adsvar_brightness"
|
||||
CONF_ADS_TYPE = "adstype"
|
||||
CONF_ADS_FACTOR = "factor"
|
||||
CONF_ADS_VALUE = "value"
|
||||
|
||||
SERVICE_WRITE_DATA_BY_NAME = 'write_data_by_name'
|
||||
SERVICE_WRITE_DATA_BY_NAME = "write_data_by_name"
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
DOMAIN: vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_DEVICE): cv.string,
|
||||
vol.Required(CONF_PORT): cv.port,
|
||||
vol.Optional(CONF_IP_ADDRESS): cv.string,
|
||||
})
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
}
|
||||
)
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
SCHEMA_SERVICE_WRITE_DATA_BY_NAME = vol.Schema({
|
||||
vol.Required(CONF_ADS_TYPE):
|
||||
vol.In([ADSTYPE_INT, ADSTYPE_UINT, ADSTYPE_BYTE]),
|
||||
SCHEMA_SERVICE_WRITE_DATA_BY_NAME = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_ADS_TYPE): vol.In([ADSTYPE_INT, ADSTYPE_UINT, ADSTYPE_BYTE]),
|
||||
vol.Required(CONF_ADS_VALUE): cv.match_all,
|
||||
vol.Required(CONF_ADS_VAR): cv.string,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Set up the ADS component."""
|
||||
import pyads
|
||||
|
||||
conf = config[DOMAIN]
|
||||
|
||||
net_id = conf.get(CONF_DEVICE)
|
||||
|
|
@ -79,8 +90,7 @@ def setup(hass, config):
|
|||
try:
|
||||
ads = AdsHub(client)
|
||||
except pyads.pyads.ADSError:
|
||||
_LOGGER.error(
|
||||
"Could not connect to ADS host (netid=%s, port=%s)", net_id, port)
|
||||
_LOGGER.error("Could not connect to ADS host (netid=%s, port=%s)", net_id, port)
|
||||
return False
|
||||
|
||||
hass.data[DATA_ADS] = ads
|
||||
|
|
@ -98,15 +108,18 @@ def setup(hass, config):
|
|||
_LOGGER.error(err)
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_WRITE_DATA_BY_NAME, handle_write_data_by_name,
|
||||
schema=SCHEMA_SERVICE_WRITE_DATA_BY_NAME)
|
||||
DOMAIN,
|
||||
SERVICE_WRITE_DATA_BY_NAME,
|
||||
handle_write_data_by_name,
|
||||
schema=SCHEMA_SERVICE_WRITE_DATA_BY_NAME,
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
# Tuple to hold data needed for notification
|
||||
NotificationItem = namedtuple(
|
||||
'NotificationItem', 'hnotify huser name plc_datatype callback'
|
||||
"NotificationItem", "hnotify huser name plc_datatype callback"
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -128,12 +141,13 @@ class AdsHub:
|
|||
_LOGGER.debug("Shutting down ADS")
|
||||
for notification_item in self._notification_items.values():
|
||||
self._client.del_device_notification(
|
||||
notification_item.hnotify,
|
||||
notification_item.huser
|
||||
notification_item.hnotify, notification_item.huser
|
||||
)
|
||||
_LOGGER.debug(
|
||||
"Deleting device notification %d, %d",
|
||||
notification_item.hnotify, notification_item.huser)
|
||||
notification_item.hnotify,
|
||||
notification_item.huser,
|
||||
)
|
||||
self._client.close()
|
||||
|
||||
def register_device(self, device):
|
||||
|
|
@ -153,18 +167,20 @@ class AdsHub:
|
|||
def add_device_notification(self, name, plc_datatype, callback):
|
||||
"""Add a notification to the ADS devices."""
|
||||
from pyads import NotificationAttrib
|
||||
|
||||
attr = NotificationAttrib(ctypes.sizeof(plc_datatype))
|
||||
|
||||
with self._lock:
|
||||
hnotify, huser = self._client.add_device_notification(
|
||||
name, attr, self._device_notification_callback)
|
||||
name, attr, self._device_notification_callback
|
||||
)
|
||||
hnotify = int(hnotify)
|
||||
|
||||
_LOGGER.debug(
|
||||
"Added device notification %d for variable %s", hnotify, name)
|
||||
_LOGGER.debug("Added device notification %d for variable %s", hnotify, name)
|
||||
|
||||
self._notification_items[hnotify] = NotificationItem(
|
||||
hnotify, huser, name, plc_datatype, callback)
|
||||
hnotify, huser, name, plc_datatype, callback
|
||||
)
|
||||
|
||||
def _device_notification_callback(self, addr, notification, huser):
|
||||
"""Handle device notifications."""
|
||||
|
|
@ -182,13 +198,13 @@ class AdsHub:
|
|||
|
||||
# Parse data to desired datatype
|
||||
if notification_item.plc_datatype == self.PLCTYPE_BOOL:
|
||||
value = bool(struct.unpack('<?', bytearray(data)[:1])[0])
|
||||
value = bool(struct.unpack("<?", bytearray(data)[:1])[0])
|
||||
elif notification_item.plc_datatype == self.PLCTYPE_INT:
|
||||
value = struct.unpack('<h', bytearray(data)[:2])[0]
|
||||
value = struct.unpack("<h", bytearray(data)[:2])[0]
|
||||
elif notification_item.plc_datatype == self.PLCTYPE_BYTE:
|
||||
value = struct.unpack('<B', bytearray(data)[:1])[0]
|
||||
value = struct.unpack("<B", bytearray(data)[:1])[0]
|
||||
elif notification_item.plc_datatype == self.PLCTYPE_UINT:
|
||||
value = struct.unpack('<H', bytearray(data)[:2])[0]
|
||||
value = struct.unpack("<H", bytearray(data)[:2])[0]
|
||||
else:
|
||||
value = bytearray(data)
|
||||
_LOGGER.warning("No callback available for this datatype")
|
||||
|
|
|
|||
|
|
@ -11,25 +11,31 @@ import logging
|
|||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, SERVICE_ALARM_TRIGGER,
|
||||
SERVICE_ALARM_DISARM, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY,
|
||||
SERVICE_ALARM_ARM_NIGHT, SERVICE_ALARM_ARM_CUSTOM_BYPASS)
|
||||
ATTR_CODE,
|
||||
ATTR_CODE_FORMAT,
|
||||
ATTR_ENTITY_ID,
|
||||
SERVICE_ALARM_TRIGGER,
|
||||
SERVICE_ALARM_DISARM,
|
||||
SERVICE_ALARM_ARM_HOME,
|
||||
SERVICE_ALARM_ARM_AWAY,
|
||||
SERVICE_ALARM_ARM_NIGHT,
|
||||
SERVICE_ALARM_ARM_CUSTOM_BYPASS,
|
||||
)
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
|
||||
DOMAIN = 'alarm_control_panel'
|
||||
DOMAIN = "alarm_control_panel"
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
ATTR_CHANGED_BY = 'changed_by'
|
||||
ATTR_CHANGED_BY = "changed_by"
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
ENTITY_ID_FORMAT = DOMAIN + ".{}"
|
||||
|
||||
ALARM_SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Optional(ATTR_CODE): cv.string,
|
||||
})
|
||||
ALARM_SERVICE_SCHEMA = vol.Schema(
|
||||
{vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Optional(ATTR_CODE): cv.string}
|
||||
)
|
||||
|
||||
|
||||
@bind_hass
|
||||
|
|
@ -108,33 +114,30 @@ def alarm_arm_custom_bypass(hass, code=None, entity_id=None):
|
|||
def async_setup(hass, config):
|
||||
"""Track states and offer events for sensors."""
|
||||
component = hass.data[DOMAIN] = EntityComponent(
|
||||
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
|
||||
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL
|
||||
)
|
||||
|
||||
yield from component.async_setup(config)
|
||||
|
||||
component.async_register_entity_service(
|
||||
SERVICE_ALARM_DISARM, ALARM_SERVICE_SCHEMA,
|
||||
'async_alarm_disarm'
|
||||
SERVICE_ALARM_DISARM, ALARM_SERVICE_SCHEMA, "async_alarm_disarm"
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
SERVICE_ALARM_ARM_HOME, ALARM_SERVICE_SCHEMA,
|
||||
'async_alarm_arm_home'
|
||||
SERVICE_ALARM_ARM_HOME, ALARM_SERVICE_SCHEMA, "async_alarm_arm_home"
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
SERVICE_ALARM_ARM_AWAY, ALARM_SERVICE_SCHEMA,
|
||||
'async_alarm_arm_away'
|
||||
SERVICE_ALARM_ARM_AWAY, ALARM_SERVICE_SCHEMA, "async_alarm_arm_away"
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
SERVICE_ALARM_ARM_NIGHT, ALARM_SERVICE_SCHEMA,
|
||||
'async_alarm_arm_night'
|
||||
SERVICE_ALARM_ARM_NIGHT, ALARM_SERVICE_SCHEMA, "async_alarm_arm_night"
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
SERVICE_ALARM_ARM_CUSTOM_BYPASS, ALARM_SERVICE_SCHEMA,
|
||||
'async_alarm_arm_custom_bypass'
|
||||
SERVICE_ALARM_ARM_CUSTOM_BYPASS,
|
||||
ALARM_SERVICE_SCHEMA,
|
||||
"async_alarm_arm_custom_bypass",
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
SERVICE_ALARM_TRIGGER, ALARM_SERVICE_SCHEMA,
|
||||
'async_alarm_trigger'
|
||||
SERVICE_ALARM_TRIGGER, ALARM_SERVICE_SCHEMA, "async_alarm_trigger"
|
||||
)
|
||||
|
||||
return True
|
||||
|
|
@ -228,14 +231,13 @@ class AlarmControlPanel(Entity):
|
|||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.async_add_executor_job(
|
||||
self.alarm_arm_custom_bypass, code)
|
||||
return self.hass.async_add_executor_job(self.alarm_arm_custom_bypass, code)
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
state_attr = {
|
||||
ATTR_CODE_FORMAT: self.code_format,
|
||||
ATTR_CHANGED_BY: self.changed_by
|
||||
ATTR_CHANGED_BY: self.changed_by,
|
||||
}
|
||||
return state_attr
|
||||
|
|
|
|||
|
|
@ -10,14 +10,17 @@ from homeassistant.components.abode import CONF_ATTRIBUTION, AbodeDevice
|
|||
from homeassistant.components.abode import DOMAIN as ABODE_DOMAIN
|
||||
from homeassistant.components.alarm_control_panel import AlarmControlPanel
|
||||
from homeassistant.const import (
|
||||
ATTR_ATTRIBUTION, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_DISARMED)
|
||||
ATTR_ATTRIBUTION,
|
||||
STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_DISARMED,
|
||||
)
|
||||
|
||||
DEPENDENCIES = ['abode']
|
||||
DEPENDENCIES = ["abode"]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ICON = 'mdi:security'
|
||||
ICON = "mdi:security"
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
|
|
@ -79,7 +82,7 @@ class AbodeAlarm(AbodeDevice, AlarmControlPanel):
|
|||
"""Return the state attributes."""
|
||||
return {
|
||||
ATTR_ATTRIBUTION: CONF_ATTRIBUTION,
|
||||
'device_id': self._device.device_id,
|
||||
'battery_backup': self._device.battery,
|
||||
'cellular_backup': self._device.is_cellular,
|
||||
"device_id": self._device.device_id,
|
||||
"battery_backup": self._device.battery,
|
||||
"cellular_backup": self._device.is_cellular,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,18 +12,20 @@ import voluptuous as vol
|
|||
import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.components.alarmdecoder import DATA_AD, SIGNAL_PANEL_MESSAGE
|
||||
from homeassistant.const import (
|
||||
ATTR_CODE, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED)
|
||||
ATTR_CODE,
|
||||
STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_DISARMED,
|
||||
STATE_ALARM_TRIGGERED,
|
||||
)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['alarmdecoder']
|
||||
DEPENDENCIES = ["alarmdecoder"]
|
||||
|
||||
SERVICE_ALARM_TOGGLE_CHIME = 'alarmdecoder_alarm_toggle_chime'
|
||||
ALARM_TOGGLE_CHIME_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_CODE): cv.string,
|
||||
})
|
||||
SERVICE_ALARM_TOGGLE_CHIME = "alarmdecoder_alarm_toggle_chime"
|
||||
ALARM_TOGGLE_CHIME_SCHEMA = vol.Schema({vol.Required(ATTR_CODE): cv.string})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
|
|
@ -37,8 +39,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||
device.alarm_toggle_chime(code)
|
||||
|
||||
hass.services.register(
|
||||
alarm.DOMAIN, SERVICE_ALARM_TOGGLE_CHIME, alarm_toggle_chime_handler,
|
||||
schema=ALARM_TOGGLE_CHIME_SCHEMA)
|
||||
alarm.DOMAIN,
|
||||
SERVICE_ALARM_TOGGLE_CHIME,
|
||||
alarm_toggle_chime_handler,
|
||||
schema=ALARM_TOGGLE_CHIME_SCHEMA,
|
||||
)
|
||||
|
||||
|
||||
class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel):
|
||||
|
|
@ -63,7 +68,8 @@ class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel):
|
|||
def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
self.hass.helpers.dispatcher.async_dispatcher_connect(
|
||||
SIGNAL_PANEL_MESSAGE, self._message_callback)
|
||||
SIGNAL_PANEL_MESSAGE, self._message_callback
|
||||
)
|
||||
|
||||
def _message_callback(self, message):
|
||||
"""Handle received messages."""
|
||||
|
|
@ -101,7 +107,7 @@ class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel):
|
|||
@property
|
||||
def code_format(self):
|
||||
"""Return one or more digits/characters."""
|
||||
return 'Number'
|
||||
return "Number"
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
|
|
@ -112,15 +118,15 @@ class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel):
|
|||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return {
|
||||
'ac_power': self._ac_power,
|
||||
'backlight_on': self._backlight_on,
|
||||
'battery_low': self._battery_low,
|
||||
'check_zone': self._check_zone,
|
||||
'chime': self._chime,
|
||||
'entry_delay_off': self._entry_delay_off,
|
||||
'programming_mode': self._programming_mode,
|
||||
'ready': self._ready,
|
||||
'zone_bypassed': self._zone_bypassed,
|
||||
"ac_power": self._ac_power,
|
||||
"backlight_on": self._backlight_on,
|
||||
"battery_low": self._battery_low,
|
||||
"check_zone": self._check_zone,
|
||||
"chime": self._chime,
|
||||
"entry_delay_off": self._entry_delay_off,
|
||||
"programming_mode": self._programming_mode,
|
||||
"ready": self._ready,
|
||||
"zone_bypassed": self._zone_bypassed,
|
||||
}
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
|
|
|
|||
|
|
@ -13,28 +13,36 @@ import voluptuous as vol
|
|||
import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
|
||||
from homeassistant.const import (
|
||||
CONF_CODE, CONF_NAME, CONF_PASSWORD, CONF_USERNAME, STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_UNKNOWN)
|
||||
CONF_CODE,
|
||||
CONF_NAME,
|
||||
CONF_PASSWORD,
|
||||
CONF_USERNAME,
|
||||
STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_DISARMED,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pyalarmdotcom==0.3.2']
|
||||
REQUIREMENTS = ["pyalarmdotcom==0.3.2"]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = 'Alarm.com'
|
||||
DEFAULT_NAME = "Alarm.com"
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Optional(CONF_CODE): cv.positive_int,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||
"""Set up a Alarm.com control panel."""
|
||||
name = config.get(CONF_NAME)
|
||||
code = config.get(CONF_CODE)
|
||||
|
|
@ -52,7 +60,8 @@ class AlarmDotCom(alarm.AlarmControlPanel):
|
|||
def __init__(self, hass, name, code, username, password):
|
||||
"""Initialize the Alarm.com status."""
|
||||
from pyalarmdotcom import Alarmdotcom
|
||||
_LOGGER.debug('Setting up Alarm.com...')
|
||||
|
||||
_LOGGER.debug("Setting up Alarm.com...")
|
||||
self._hass = hass
|
||||
self._name = name
|
||||
self._code = str(code) if code else None
|
||||
|
|
@ -60,8 +69,7 @@ class AlarmDotCom(alarm.AlarmControlPanel):
|
|||
self._password = password
|
||||
self._websession = async_get_clientsession(self._hass)
|
||||
self._state = STATE_UNKNOWN
|
||||
self._alarm = Alarmdotcom(
|
||||
username, password, self._websession, hass.loop)
|
||||
self._alarm = Alarmdotcom(username, password, self._websession, hass.loop)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_login(self):
|
||||
|
|
@ -84,27 +92,25 @@ class AlarmDotCom(alarm.AlarmControlPanel):
|
|||
"""Return one or more digits/characters."""
|
||||
if self._code is None:
|
||||
return None
|
||||
if isinstance(self._code, str) and re.search('^\\d+$', self._code):
|
||||
return 'Number'
|
||||
return 'Any'
|
||||
if isinstance(self._code, str) and re.search("^\\d+$", self._code):
|
||||
return "Number"
|
||||
return "Any"
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
if self._alarm.state.lower() == 'disarmed':
|
||||
if self._alarm.state.lower() == "disarmed":
|
||||
return STATE_ALARM_DISARMED
|
||||
if self._alarm.state.lower() == 'armed stay':
|
||||
if self._alarm.state.lower() == "armed stay":
|
||||
return STATE_ALARM_ARMED_HOME
|
||||
if self._alarm.state.lower() == 'armed away':
|
||||
if self._alarm.state.lower() == "armed away":
|
||||
return STATE_ALARM_ARMED_AWAY
|
||||
return STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return {
|
||||
'sensor_status': self._alarm.sensor_status
|
||||
}
|
||||
return {"sensor_status": self._alarm.sensor_status}
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_disarm(self, code=None):
|
||||
|
|
|
|||
|
|
@ -12,30 +12,40 @@ import homeassistant.helpers.config_validation as cv
|
|||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.components.alarm_control_panel import (
|
||||
AlarmControlPanel, PLATFORM_SCHEMA)
|
||||
AlarmControlPanel,
|
||||
PLATFORM_SCHEMA,
|
||||
)
|
||||
from homeassistant.components.arlo import (
|
||||
DATA_ARLO, CONF_ATTRIBUTION, SIGNAL_UPDATE_ARLO)
|
||||
DATA_ARLO,
|
||||
CONF_ATTRIBUTION,
|
||||
SIGNAL_UPDATE_ARLO,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_ATTRIBUTION, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_DISARMED)
|
||||
ATTR_ATTRIBUTION,
|
||||
STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_DISARMED,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ARMED = 'armed'
|
||||
ARMED = "armed"
|
||||
|
||||
CONF_HOME_MODE_NAME = 'home_mode_name'
|
||||
CONF_AWAY_MODE_NAME = 'away_mode_name'
|
||||
CONF_HOME_MODE_NAME = "home_mode_name"
|
||||
CONF_AWAY_MODE_NAME = "away_mode_name"
|
||||
|
||||
DEPENDENCIES = ['arlo']
|
||||
DEPENDENCIES = ["arlo"]
|
||||
|
||||
DISARMED = 'disarmed'
|
||||
DISARMED = "disarmed"
|
||||
|
||||
ICON = 'mdi:security'
|
||||
ICON = "mdi:security"
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_HOME_MODE_NAME, default=ARMED): cv.string,
|
||||
vol.Optional(CONF_AWAY_MODE_NAME, default=ARMED): cv.string,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
|
|
@ -49,8 +59,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||
away_mode_name = config.get(CONF_AWAY_MODE_NAME)
|
||||
base_stations = []
|
||||
for base_station in arlo.base_stations:
|
||||
base_stations.append(ArloBaseStation(base_station, home_mode_name,
|
||||
away_mode_name))
|
||||
base_stations.append(
|
||||
ArloBaseStation(base_station, home_mode_name, away_mode_name)
|
||||
)
|
||||
add_entities(base_stations, True)
|
||||
|
||||
|
||||
|
|
@ -71,8 +82,7 @@ class ArloBaseStation(AlarmControlPanel):
|
|||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
async_dispatcher_connect(
|
||||
self.hass, SIGNAL_UPDATE_ARLO, self._update_callback)
|
||||
async_dispatcher_connect(self.hass, SIGNAL_UPDATE_ARLO, self._update_callback)
|
||||
|
||||
@callback
|
||||
def _update_callback(self):
|
||||
|
|
@ -115,7 +125,7 @@ class ArloBaseStation(AlarmControlPanel):
|
|||
"""Return the state attributes."""
|
||||
return {
|
||||
ATTR_ATTRIBUTION: CONF_ATTRIBUTION,
|
||||
'device_id': self._base_station.device_id
|
||||
"device_id": self._base_station.device_id,
|
||||
}
|
||||
|
||||
def _get_state_from_mode(self, mode):
|
||||
|
|
|
|||
|
|
@ -8,10 +8,14 @@ import logging
|
|||
|
||||
from homeassistant.components.alarm_control_panel import AlarmControlPanel
|
||||
from homeassistant.components.canary import DATA_CANARY
|
||||
from homeassistant.const import STATE_ALARM_DISARMED, STATE_ALARM_ARMED_AWAY, \
|
||||
STATE_ALARM_ARMED_NIGHT, STATE_ALARM_ARMED_HOME
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_DISARMED,
|
||||
STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_NIGHT,
|
||||
STATE_ALARM_ARMED_HOME,
|
||||
)
|
||||
|
||||
DEPENDENCIES = ['canary']
|
||||
DEPENDENCIES = ["canary"]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -44,8 +48,11 @@ class CanaryAlarm(AlarmControlPanel):
|
|||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
from canary.api import LOCATION_MODE_AWAY, LOCATION_MODE_HOME, \
|
||||
LOCATION_MODE_NIGHT
|
||||
from canary.api import (
|
||||
LOCATION_MODE_AWAY,
|
||||
LOCATION_MODE_HOME,
|
||||
LOCATION_MODE_NIGHT,
|
||||
)
|
||||
|
||||
location = self._data.get_location(self._location_id)
|
||||
|
||||
|
|
@ -65,27 +72,27 @@ class CanaryAlarm(AlarmControlPanel):
|
|||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
location = self._data.get_location(self._location_id)
|
||||
return {
|
||||
'private': location.is_private
|
||||
}
|
||||
return {"private": location.is_private}
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
location = self._data.get_location(self._location_id)
|
||||
self._data.set_location_mode(self._location_id, location.mode.name,
|
||||
True)
|
||||
self._data.set_location_mode(self._location_id, location.mode.name, True)
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
from canary.api import LOCATION_MODE_HOME
|
||||
|
||||
self._data.set_location_mode(self._location_id, LOCATION_MODE_HOME)
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
from canary.api import LOCATION_MODE_AWAY
|
||||
|
||||
self._data.set_location_mode(self._location_id, LOCATION_MODE_AWAY)
|
||||
|
||||
def alarm_arm_night(self, code=None):
|
||||
"""Send arm night command."""
|
||||
from canary.api import LOCATION_MODE_NIGHT
|
||||
|
||||
self._data.set_location_mode(self._location_id, LOCATION_MODE_NIGHT)
|
||||
|
|
|
|||
|
|
@ -14,25 +14,33 @@ import voluptuous as vol
|
|||
import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_NAME, CONF_PORT, STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_UNKNOWN)
|
||||
CONF_HOST,
|
||||
CONF_NAME,
|
||||
CONF_PORT,
|
||||
STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_DISARMED,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['concord232==0.15']
|
||||
REQUIREMENTS = ["concord232==0.15"]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_HOST = 'localhost'
|
||||
DEFAULT_NAME = 'CONCORD232'
|
||||
DEFAULT_HOST = "localhost"
|
||||
DEFAULT_NAME = "CONCORD232"
|
||||
DEFAULT_PORT = 5007
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=10)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
|
|
@ -41,7 +49,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||
host = config.get(CONF_HOST)
|
||||
port = config.get(CONF_PORT)
|
||||
|
||||
url = 'http://{}:{}'.format(host, port)
|
||||
url = "http://{}:{}".format(host, port)
|
||||
|
||||
try:
|
||||
add_entities([Concord232Alarm(hass, url, name)])
|
||||
|
|
@ -80,7 +88,7 @@ class Concord232Alarm(alarm.AlarmControlPanel):
|
|||
@property
|
||||
def code_format(self):
|
||||
"""Return the characters if code is defined."""
|
||||
return 'Number'
|
||||
return "Number"
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
|
|
@ -92,16 +100,18 @@ class Concord232Alarm(alarm.AlarmControlPanel):
|
|||
try:
|
||||
part = self._alarm.list_partitions()[0]
|
||||
except requests.exceptions.ConnectionError as ex:
|
||||
_LOGGER.error("Unable to connect to %(host)s: %(reason)s",
|
||||
dict(host=self._url, reason=ex))
|
||||
_LOGGER.error(
|
||||
"Unable to connect to %(host)s: %(reason)s",
|
||||
dict(host=self._url, reason=ex),
|
||||
)
|
||||
newstate = STATE_UNKNOWN
|
||||
except IndexError:
|
||||
_LOGGER.error("Concord232 reports no partitions")
|
||||
newstate = STATE_UNKNOWN
|
||||
|
||||
if part['arming_level'] == 'Off':
|
||||
if part["arming_level"] == "Off":
|
||||
newstate = STATE_ALARM_DISARMED
|
||||
elif 'Home' in part['arming_level']:
|
||||
elif "Home" in part["arming_level"]:
|
||||
newstate = STATE_ALARM_ARMED_HOME
|
||||
else:
|
||||
newstate = STATE_ALARM_ARMED_AWAY
|
||||
|
|
@ -117,8 +127,8 @@ class Concord232Alarm(alarm.AlarmControlPanel):
|
|||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
self._alarm.arm('stay')
|
||||
self._alarm.arm("stay")
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
self._alarm.arm('away')
|
||||
self._alarm.arm("away")
|
||||
|
|
|
|||
|
|
@ -7,16 +7,29 @@ https://home-assistant.io/components/demo/
|
|||
import datetime
|
||||
from homeassistant.components.alarm_control_panel import manual
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_CUSTOM_BYPASS,
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT,
|
||||
STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, CONF_DELAY_TIME,
|
||||
CONF_PENDING_TIME, CONF_TRIGGER_TIME)
|
||||
STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_CUSTOM_BYPASS,
|
||||
STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_ARMED_NIGHT,
|
||||
STATE_ALARM_DISARMED,
|
||||
STATE_ALARM_TRIGGERED,
|
||||
CONF_DELAY_TIME,
|
||||
CONF_PENDING_TIME,
|
||||
CONF_TRIGGER_TIME,
|
||||
)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the Demo alarm control panel platform."""
|
||||
add_entities([
|
||||
manual.ManualAlarm(hass, 'Alarm', '1234', None, False, {
|
||||
add_entities(
|
||||
[
|
||||
manual.ManualAlarm(
|
||||
hass,
|
||||
"Alarm",
|
||||
"1234",
|
||||
None,
|
||||
False,
|
||||
{
|
||||
STATE_ALARM_ARMED_AWAY: {
|
||||
CONF_DELAY_TIME: datetime.timedelta(seconds=0),
|
||||
CONF_PENDING_TIME: datetime.timedelta(seconds=5),
|
||||
|
|
@ -42,7 +55,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||
CONF_TRIGGER_TIME: datetime.timedelta(seconds=10),
|
||||
},
|
||||
STATE_ALARM_TRIGGERED: {
|
||||
CONF_PENDING_TIME: datetime.timedelta(seconds=5),
|
||||
CONF_PENDING_TIME: datetime.timedelta(seconds=5)
|
||||
},
|
||||
}),
|
||||
])
|
||||
},
|
||||
)
|
||||
]
|
||||
)
|
||||
|
|
|
|||
|
|
@ -11,26 +11,33 @@ import requests
|
|||
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_TRIGGERED,
|
||||
STATE_ALARM_ARMED_NIGHT)
|
||||
from homeassistant.components.egardia import (
|
||||
EGARDIA_DEVICE, EGARDIA_SERVER,
|
||||
REPORT_SERVER_CODES_IGNORE, CONF_REPORT_SERVER_CODES,
|
||||
CONF_REPORT_SERVER_ENABLED, CONF_REPORT_SERVER_PORT
|
||||
STATE_ALARM_DISARMED,
|
||||
STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_TRIGGERED,
|
||||
STATE_ALARM_ARMED_NIGHT,
|
||||
)
|
||||
DEPENDENCIES = ['egardia']
|
||||
from homeassistant.components.egardia import (
|
||||
EGARDIA_DEVICE,
|
||||
EGARDIA_SERVER,
|
||||
REPORT_SERVER_CODES_IGNORE,
|
||||
CONF_REPORT_SERVER_CODES,
|
||||
CONF_REPORT_SERVER_ENABLED,
|
||||
CONF_REPORT_SERVER_PORT,
|
||||
)
|
||||
|
||||
DEPENDENCIES = ["egardia"]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
STATES = {
|
||||
'ARM': STATE_ALARM_ARMED_AWAY,
|
||||
'DAY HOME': STATE_ALARM_ARMED_HOME,
|
||||
'DISARM': STATE_ALARM_DISARMED,
|
||||
'ARMHOME': STATE_ALARM_ARMED_HOME,
|
||||
'HOME': STATE_ALARM_ARMED_HOME,
|
||||
'NIGHT HOME': STATE_ALARM_ARMED_NIGHT,
|
||||
'TRIGGERED': STATE_ALARM_TRIGGERED
|
||||
"ARM": STATE_ALARM_ARMED_AWAY,
|
||||
"DAY HOME": STATE_ALARM_ARMED_HOME,
|
||||
"DISARM": STATE_ALARM_DISARMED,
|
||||
"ARMHOME": STATE_ALARM_ARMED_HOME,
|
||||
"HOME": STATE_ALARM_ARMED_HOME,
|
||||
"NIGHT HOME": STATE_ALARM_ARMED_NIGHT,
|
||||
"TRIGGERED": STATE_ALARM_TRIGGERED,
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -39,11 +46,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||
if discovery_info is None:
|
||||
return
|
||||
device = EgardiaAlarm(
|
||||
discovery_info['name'],
|
||||
discovery_info["name"],
|
||||
hass.data[EGARDIA_DEVICE],
|
||||
discovery_info[CONF_REPORT_SERVER_ENABLED],
|
||||
discovery_info.get(CONF_REPORT_SERVER_CODES),
|
||||
discovery_info[CONF_REPORT_SERVER_PORT])
|
||||
discovery_info[CONF_REPORT_SERVER_PORT],
|
||||
)
|
||||
# add egardia alarm device
|
||||
add_entities([device], True)
|
||||
|
||||
|
|
@ -51,8 +59,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||
class EgardiaAlarm(alarm.AlarmControlPanel):
|
||||
"""Representation of a Egardia alarm."""
|
||||
|
||||
def __init__(self, name, egardiasystem,
|
||||
rs_enabled=False, rs_codes=None, rs_port=52010):
|
||||
def __init__(
|
||||
self, name, egardiasystem, rs_enabled=False, rs_codes=None, rs_port=52010
|
||||
):
|
||||
"""Initialize the Egardia alarm."""
|
||||
self._name = name
|
||||
self._egardiasystem = egardiasystem
|
||||
|
|
@ -66,8 +75,7 @@ class EgardiaAlarm(alarm.AlarmControlPanel):
|
|||
"""Add Egardiaserver callback if enabled."""
|
||||
if self._rs_enabled:
|
||||
_LOGGER.debug("Registering callback to Egardiaserver")
|
||||
self.hass.data[EGARDIA_SERVER].register_callback(
|
||||
self.handle_status_event)
|
||||
self.hass.data[EGARDIA_SERVER].register_callback(self.handle_status_event)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
|
@ -88,7 +96,7 @@ class EgardiaAlarm(alarm.AlarmControlPanel):
|
|||
|
||||
def handle_status_event(self, event):
|
||||
"""Handle the Egardia system status event."""
|
||||
statuscode = event.get('status')
|
||||
statuscode = event.get("status")
|
||||
if statuscode is not None:
|
||||
status = self.lookupstatusfromcode(statuscode)
|
||||
self.parsestatus(status)
|
||||
|
|
@ -96,10 +104,15 @@ class EgardiaAlarm(alarm.AlarmControlPanel):
|
|||
|
||||
def lookupstatusfromcode(self, statuscode):
|
||||
"""Look at the rs_codes and returns the status from the code."""
|
||||
status = next((
|
||||
status_group.upper() for status_group, codes
|
||||
in self._rs_codes.items() for code in codes
|
||||
if statuscode == code), 'UNKNOWN')
|
||||
status = next(
|
||||
(
|
||||
status_group.upper()
|
||||
for status_group, codes in self._rs_codes.items()
|
||||
for code in codes
|
||||
if statuscode == code
|
||||
),
|
||||
"UNKNOWN",
|
||||
)
|
||||
return status
|
||||
|
||||
def parsestatus(self, status):
|
||||
|
|
@ -124,21 +137,29 @@ class EgardiaAlarm(alarm.AlarmControlPanel):
|
|||
try:
|
||||
self._egardiasystem.alarm_disarm()
|
||||
except requests.exceptions.RequestException as err:
|
||||
_LOGGER.error("Egardia device exception occurred when "
|
||||
"sending disarm command: %s", err)
|
||||
_LOGGER.error(
|
||||
"Egardia device exception occurred when " "sending disarm command: %s",
|
||||
err,
|
||||
)
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
try:
|
||||
self._egardiasystem.alarm_arm_home()
|
||||
except requests.exceptions.RequestException as err:
|
||||
_LOGGER.error("Egardia device exception occurred when "
|
||||
"sending arm home command: %s", err)
|
||||
_LOGGER.error(
|
||||
"Egardia device exception occurred when "
|
||||
"sending arm home command: %s",
|
||||
err,
|
||||
)
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
try:
|
||||
self._egardiasystem.alarm_arm_away()
|
||||
except requests.exceptions.RequestException as err:
|
||||
_LOGGER.error("Egardia device exception occurred when "
|
||||
"sending arm away command: %s", err)
|
||||
_LOGGER.error(
|
||||
"Egardia device exception occurred when "
|
||||
"sending arm away command: %s",
|
||||
err,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -14,29 +14,43 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
|||
import homeassistant.components.alarm_control_panel as alarm
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.envisalink import (
|
||||
DATA_EVL, EnvisalinkDevice, PARTITION_SCHEMA, CONF_CODE, CONF_PANIC,
|
||||
CONF_PARTITIONNAME, SIGNAL_KEYPAD_UPDATE, SIGNAL_PARTITION_UPDATE)
|
||||
DATA_EVL,
|
||||
EnvisalinkDevice,
|
||||
PARTITION_SCHEMA,
|
||||
CONF_CODE,
|
||||
CONF_PANIC,
|
||||
CONF_PARTITIONNAME,
|
||||
SIGNAL_KEYPAD_UPDATE,
|
||||
SIGNAL_PARTITION_UPDATE,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
|
||||
STATE_UNKNOWN, STATE_ALARM_TRIGGERED, STATE_ALARM_PENDING, ATTR_ENTITY_ID)
|
||||
STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_DISARMED,
|
||||
STATE_UNKNOWN,
|
||||
STATE_ALARM_TRIGGERED,
|
||||
STATE_ALARM_PENDING,
|
||||
ATTR_ENTITY_ID,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['envisalink']
|
||||
DEPENDENCIES = ["envisalink"]
|
||||
|
||||
SERVICE_ALARM_KEYPRESS = 'envisalink_alarm_keypress'
|
||||
ATTR_KEYPRESS = 'keypress'
|
||||
ALARM_KEYPRESS_SCHEMA = vol.Schema({
|
||||
SERVICE_ALARM_KEYPRESS = "envisalink_alarm_keypress"
|
||||
ATTR_KEYPRESS = "keypress"
|
||||
ALARM_KEYPRESS_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Required(ATTR_KEYPRESS): cv.string
|
||||
})
|
||||
vol.Required(ATTR_KEYPRESS): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||
"""Perform the setup for Envisalink alarm panels."""
|
||||
configured_partitions = discovery_info['partitions']
|
||||
configured_partitions = discovery_info["partitions"]
|
||||
code = discovery_info[CONF_CODE]
|
||||
panic_type = discovery_info[CONF_PANIC]
|
||||
|
||||
|
|
@ -49,8 +63,8 @@ def async_setup_platform(hass, config, async_add_entities,
|
|||
device_config_data[CONF_PARTITIONNAME],
|
||||
code,
|
||||
panic_type,
|
||||
hass.data[DATA_EVL].alarm_state['partition'][part_num],
|
||||
hass.data[DATA_EVL]
|
||||
hass.data[DATA_EVL].alarm_state["partition"][part_num],
|
||||
hass.data[DATA_EVL],
|
||||
)
|
||||
devices.append(device)
|
||||
|
||||
|
|
@ -62,15 +76,19 @@ def async_setup_platform(hass, config, async_add_entities,
|
|||
entity_ids = service.data.get(ATTR_ENTITY_ID)
|
||||
keypress = service.data.get(ATTR_KEYPRESS)
|
||||
|
||||
target_devices = [device for device in devices
|
||||
if device.entity_id in entity_ids]
|
||||
target_devices = [
|
||||
device for device in devices if device.entity_id in entity_ids
|
||||
]
|
||||
|
||||
for device in target_devices:
|
||||
device.async_alarm_keypress(keypress)
|
||||
|
||||
hass.services.async_register(
|
||||
alarm.DOMAIN, SERVICE_ALARM_KEYPRESS, alarm_keypress_handler,
|
||||
schema=ALARM_KEYPRESS_SCHEMA)
|
||||
alarm.DOMAIN,
|
||||
SERVICE_ALARM_KEYPRESS,
|
||||
alarm_keypress_handler,
|
||||
schema=ALARM_KEYPRESS_SCHEMA,
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
|
@ -78,8 +96,9 @@ def async_setup_platform(hass, config, async_add_entities,
|
|||
class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
|
||||
"""Representation of an Envisalink-based alarm panel."""
|
||||
|
||||
def __init__(self, hass, partition_number, alarm_name, code, panic_type,
|
||||
info, controller):
|
||||
def __init__(
|
||||
self, hass, partition_number, alarm_name, code, panic_type, info, controller
|
||||
):
|
||||
"""Initialize the alarm panel."""
|
||||
self._partition_number = partition_number
|
||||
self._code = code
|
||||
|
|
@ -91,10 +110,10 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
|
|||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
async_dispatcher_connect(self.hass, SIGNAL_KEYPAD_UPDATE, self._update_callback)
|
||||
async_dispatcher_connect(
|
||||
self.hass, SIGNAL_KEYPAD_UPDATE, self._update_callback)
|
||||
async_dispatcher_connect(
|
||||
self.hass, SIGNAL_PARTITION_UPDATE, self._update_callback)
|
||||
self.hass, SIGNAL_PARTITION_UPDATE, self._update_callback
|
||||
)
|
||||
|
||||
@callback
|
||||
def _update_callback(self, partition):
|
||||
|
|
@ -107,24 +126,24 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
|
|||
"""Regex for code format or None if no code is required."""
|
||||
if self._code:
|
||||
return None
|
||||
return 'Number'
|
||||
return "Number"
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
state = STATE_UNKNOWN
|
||||
|
||||
if self._info['status']['alarm']:
|
||||
if self._info["status"]["alarm"]:
|
||||
state = STATE_ALARM_TRIGGERED
|
||||
elif self._info['status']['armed_away']:
|
||||
elif self._info["status"]["armed_away"]:
|
||||
state = STATE_ALARM_ARMED_AWAY
|
||||
elif self._info['status']['armed_stay']:
|
||||
elif self._info["status"]["armed_stay"]:
|
||||
state = STATE_ALARM_ARMED_HOME
|
||||
elif self._info['status']['exit_delay']:
|
||||
elif self._info["status"]["exit_delay"]:
|
||||
state = STATE_ALARM_PENDING
|
||||
elif self._info['status']['entry_delay']:
|
||||
elif self._info["status"]["entry_delay"]:
|
||||
state = STATE_ALARM_PENDING
|
||||
elif self._info['status']['alpha']:
|
||||
elif self._info["status"]["alpha"]:
|
||||
state = STATE_ALARM_DISARMED
|
||||
return state
|
||||
|
||||
|
|
@ -132,31 +151,35 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
|
|||
def async_alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
if code:
|
||||
self.hass.data[DATA_EVL].disarm_partition(
|
||||
str(code), self._partition_number)
|
||||
self.hass.data[DATA_EVL].disarm_partition(str(code), self._partition_number)
|
||||
else:
|
||||
self.hass.data[DATA_EVL].disarm_partition(
|
||||
str(self._code), self._partition_number)
|
||||
str(self._code), self._partition_number
|
||||
)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
if code:
|
||||
self.hass.data[DATA_EVL].arm_stay_partition(
|
||||
str(code), self._partition_number)
|
||||
str(code), self._partition_number
|
||||
)
|
||||
else:
|
||||
self.hass.data[DATA_EVL].arm_stay_partition(
|
||||
str(self._code), self._partition_number)
|
||||
str(self._code), self._partition_number
|
||||
)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
if code:
|
||||
self.hass.data[DATA_EVL].arm_away_partition(
|
||||
str(code), self._partition_number)
|
||||
str(code), self._partition_number
|
||||
)
|
||||
else:
|
||||
self.hass.data[DATA_EVL].arm_away_partition(
|
||||
str(self._code), self._partition_number)
|
||||
str(self._code), self._partition_number
|
||||
)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_trigger(self, code=None):
|
||||
|
|
@ -168,4 +191,5 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
|
|||
"""Send custom keypress."""
|
||||
if keypress:
|
||||
self.hass.data[DATA_EVL].keypresses_to_partition(
|
||||
self._partition_number, keypress)
|
||||
self._partition_number, keypress
|
||||
)
|
||||
|
|
|
|||
|
|
@ -9,22 +9,26 @@ import logging
|
|||
|
||||
from homeassistant.components.alarm_control_panel import AlarmControlPanel
|
||||
from homeassistant.components.homematicip_cloud import (
|
||||
HMIPC_HAPID, HomematicipGenericDevice)
|
||||
HMIPC_HAPID,
|
||||
HomematicipGenericDevice,
|
||||
)
|
||||
from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
|
||||
STATE_ALARM_TRIGGERED)
|
||||
STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_DISARMED,
|
||||
STATE_ALARM_TRIGGERED,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['homematicip_cloud']
|
||||
DEPENDENCIES = ["homematicip_cloud"]
|
||||
|
||||
HMIP_ZONE_AWAY = 'EXTERNAL'
|
||||
HMIP_ZONE_HOME = 'INTERNAL'
|
||||
HMIP_ZONE_AWAY = "EXTERNAL"
|
||||
HMIP_ZONE_HOME = "INTERNAL"
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
hass, config, async_add_entities, discovery_info=None):
|
||||
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||
"""Set up the HomematicIP Cloud alarm control devices."""
|
||||
pass
|
||||
|
||||
|
|
@ -48,8 +52,8 @@ class HomematicipSecurityZone(HomematicipGenericDevice, AlarmControlPanel):
|
|||
|
||||
def __init__(self, home, device):
|
||||
"""Initialize the security zone group."""
|
||||
device.modelType = 'Group-SecurityZone'
|
||||
device.windowState = ''
|
||||
device.modelType = "Group-SecurityZone"
|
||||
device.windowState = ""
|
||||
super().__init__(home, device)
|
||||
|
||||
@property
|
||||
|
|
@ -58,8 +62,11 @@ class HomematicipSecurityZone(HomematicipGenericDevice, AlarmControlPanel):
|
|||
from homematicip.base.enums import WindowState
|
||||
|
||||
if self._device.active:
|
||||
if (self._device.sabotage or self._device.motionDetected or
|
||||
self._device.windowState == WindowState.OPEN):
|
||||
if (
|
||||
self._device.sabotage
|
||||
or self._device.motionDetected
|
||||
or self._device.windowState == WindowState.OPEN
|
||||
):
|
||||
return STATE_ALARM_TRIGGERED
|
||||
|
||||
active = self._home.get_security_zones_activation()
|
||||
|
|
|
|||
|
|
@ -11,33 +11,40 @@ import voluptuous as vol
|
|||
import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME, STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED)
|
||||
CONF_HOST,
|
||||
CONF_NAME,
|
||||
CONF_PASSWORD,
|
||||
CONF_USERNAME,
|
||||
STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_DISARMED,
|
||||
)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pyialarm==0.2']
|
||||
REQUIREMENTS = ["pyialarm==0.2"]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = 'iAlarm'
|
||||
DEFAULT_NAME = "iAlarm"
|
||||
|
||||
|
||||
def no_application_protocol(value):
|
||||
"""Validate that value is without the application protocol."""
|
||||
protocol_separator = "://"
|
||||
if not value or protocol_separator in value:
|
||||
raise vol.Invalid(
|
||||
'Invalid host, {} is not allowed'.format(protocol_separator))
|
||||
raise vol.Invalid("Invalid host, {} is not allowed".format(protocol_separator))
|
||||
|
||||
return value
|
||||
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_HOST): vol.All(cv.string, no_application_protocol),
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
|
|
@ -47,7 +54,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||
password = config.get(CONF_PASSWORD)
|
||||
host = config.get(CONF_HOST)
|
||||
|
||||
url = 'http://{}'.format(host)
|
||||
url = "http://{}".format(host)
|
||||
ialarm = IAlarmPanel(name, username, password, url)
|
||||
add_entities([ialarm], True)
|
||||
|
||||
|
|
@ -79,7 +86,7 @@ class IAlarmPanel(alarm.AlarmControlPanel):
|
|||
def update(self):
|
||||
"""Return the state of the device."""
|
||||
status = self._client.get_status()
|
||||
_LOGGER.debug('iAlarm status: %s', status)
|
||||
_LOGGER.debug("iAlarm status: %s", status)
|
||||
if status:
|
||||
status = int(status)
|
||||
|
||||
|
|
|
|||
|
|
@ -10,25 +10,37 @@ import re
|
|||
import voluptuous as vol
|
||||
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.components.alarm_control_panel import (
|
||||
DOMAIN, PLATFORM_SCHEMA)
|
||||
from homeassistant.components.alarm_control_panel import DOMAIN, PLATFORM_SCHEMA
|
||||
from homeassistant.components.ifttt import (
|
||||
ATTR_EVENT, DOMAIN as IFTTT_DOMAIN, SERVICE_TRIGGER)
|
||||
ATTR_EVENT,
|
||||
DOMAIN as IFTTT_DOMAIN,
|
||||
SERVICE_TRIGGER,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, ATTR_STATE, CONF_NAME, CONF_CODE,
|
||||
CONF_OPTIMISTIC, STATE_ALARM_DISARMED, STATE_ALARM_ARMED_NIGHT,
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY)
|
||||
ATTR_ENTITY_ID,
|
||||
ATTR_STATE,
|
||||
CONF_NAME,
|
||||
CONF_CODE,
|
||||
CONF_OPTIMISTIC,
|
||||
STATE_ALARM_DISARMED,
|
||||
STATE_ALARM_ARMED_NIGHT,
|
||||
STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_ARMED_AWAY,
|
||||
)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
DEPENDENCIES = ['ifttt']
|
||||
DEPENDENCIES = ["ifttt"]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ALLOWED_STATES = [
|
||||
STATE_ALARM_DISARMED, STATE_ALARM_ARMED_NIGHT,
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME]
|
||||
STATE_ALARM_DISARMED,
|
||||
STATE_ALARM_ARMED_NIGHT,
|
||||
STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME,
|
||||
]
|
||||
|
||||
DATA_IFTTT_ALARM = 'ifttt_alarm'
|
||||
DATA_IFTTT_ALARM = "ifttt_alarm"
|
||||
DEFAULT_NAME = "Home"
|
||||
|
||||
CONF_EVENT_AWAY = "event_arm_away"
|
||||
|
|
@ -41,7 +53,8 @@ DEFAULT_EVENT_HOME = "alarm_arm_home"
|
|||
DEFAULT_EVENT_NIGHT = "alarm_arm_night"
|
||||
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_EVENT_AWAY, default=DEFAULT_EVENT_AWAY): cv.string,
|
||||
|
|
@ -49,14 +62,14 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||
vol.Optional(CONF_EVENT_NIGHT, default=DEFAULT_EVENT_NIGHT): cv.string,
|
||||
vol.Optional(CONF_EVENT_DISARM, default=DEFAULT_EVENT_DISARM): cv.string,
|
||||
vol.Optional(CONF_OPTIMISTIC, default=False): cv.boolean,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
SERVICE_PUSH_ALARM_STATE = "ifttt_push_alarm_state"
|
||||
|
||||
PUSH_ALARM_STATE_SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Required(ATTR_STATE): cv.string,
|
||||
})
|
||||
PUSH_ALARM_STATE_SERVICE_SCHEMA = vol.Schema(
|
||||
{vol.Required(ATTR_ENTITY_ID): cv.entity_ids, vol.Required(ATTR_STATE): cv.string}
|
||||
)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
|
|
@ -72,8 +85,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||
event_disarm = config.get(CONF_EVENT_DISARM)
|
||||
optimistic = config.get(CONF_OPTIMISTIC)
|
||||
|
||||
alarmpanel = IFTTTAlarmPanel(name, code, event_away, event_home,
|
||||
event_night, event_disarm, optimistic)
|
||||
alarmpanel = IFTTTAlarmPanel(
|
||||
name, code, event_away, event_home, event_night, event_disarm, optimistic
|
||||
)
|
||||
hass.data[DATA_IFTTT_ALARM].append(alarmpanel)
|
||||
add_entities([alarmpanel])
|
||||
|
||||
|
|
@ -89,15 +103,20 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||
device.push_alarm_state(state)
|
||||
device.async_schedule_update_ha_state()
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_PUSH_ALARM_STATE, push_state_update,
|
||||
schema=PUSH_ALARM_STATE_SERVICE_SCHEMA)
|
||||
hass.services.register(
|
||||
DOMAIN,
|
||||
SERVICE_PUSH_ALARM_STATE,
|
||||
push_state_update,
|
||||
schema=PUSH_ALARM_STATE_SERVICE_SCHEMA,
|
||||
)
|
||||
|
||||
|
||||
class IFTTTAlarmPanel(alarm.AlarmControlPanel):
|
||||
"""Representation of an alarm control panel controlled through IFTTT."""
|
||||
|
||||
def __init__(self, name, code, event_away, event_home, event_night,
|
||||
event_disarm, optimistic):
|
||||
def __init__(
|
||||
self, name, code, event_away, event_home, event_night, event_disarm, optimistic
|
||||
):
|
||||
"""Initialize the alarm control panel."""
|
||||
self._name = name
|
||||
self._code = code
|
||||
|
|
@ -128,9 +147,9 @@ class IFTTTAlarmPanel(alarm.AlarmControlPanel):
|
|||
"""Return one or more digits/characters."""
|
||||
if self._code is None:
|
||||
return None
|
||||
if isinstance(self._code, str) and re.search('^\\d+$', self._code):
|
||||
return 'Number'
|
||||
return 'Any'
|
||||
if isinstance(self._code, str) and re.search("^\\d+$", self._code):
|
||||
return "Number"
|
||||
return "Any"
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
|
|
|
|||
|
|
@ -13,37 +13,54 @@ import voluptuous as vol
|
|||
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.const import (
|
||||
CONF_CODE, CONF_DELAY_TIME, CONF_DISARM_AFTER_TRIGGER, CONF_NAME,
|
||||
CONF_PENDING_TIME, CONF_PLATFORM, CONF_TRIGGER_TIME,
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_CUSTOM_BYPASS,
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED,
|
||||
STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED)
|
||||
CONF_CODE,
|
||||
CONF_DELAY_TIME,
|
||||
CONF_DISARM_AFTER_TRIGGER,
|
||||
CONF_NAME,
|
||||
CONF_PENDING_TIME,
|
||||
CONF_PLATFORM,
|
||||
CONF_TRIGGER_TIME,
|
||||
STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_CUSTOM_BYPASS,
|
||||
STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_ARMED_NIGHT,
|
||||
STATE_ALARM_DISARMED,
|
||||
STATE_ALARM_PENDING,
|
||||
STATE_ALARM_TRIGGERED,
|
||||
)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.event import track_point_in_time
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_CODE_TEMPLATE = 'code_template'
|
||||
CONF_CODE_TEMPLATE = "code_template"
|
||||
|
||||
DEFAULT_ALARM_NAME = 'HA Alarm'
|
||||
DEFAULT_ALARM_NAME = "HA Alarm"
|
||||
DEFAULT_DELAY_TIME = datetime.timedelta(seconds=0)
|
||||
DEFAULT_PENDING_TIME = datetime.timedelta(seconds=60)
|
||||
DEFAULT_TRIGGER_TIME = datetime.timedelta(seconds=120)
|
||||
DEFAULT_DISARM_AFTER_TRIGGER = False
|
||||
|
||||
SUPPORTED_STATES = [STATE_ALARM_DISARMED, STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT,
|
||||
STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_TRIGGERED]
|
||||
SUPPORTED_STATES = [
|
||||
STATE_ALARM_DISARMED,
|
||||
STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_ARMED_NIGHT,
|
||||
STATE_ALARM_ARMED_CUSTOM_BYPASS,
|
||||
STATE_ALARM_TRIGGERED,
|
||||
]
|
||||
|
||||
SUPPORTED_PRETRIGGER_STATES = [state for state in SUPPORTED_STATES
|
||||
if state != STATE_ALARM_TRIGGERED]
|
||||
SUPPORTED_PRETRIGGER_STATES = [
|
||||
state for state in SUPPORTED_STATES if state != STATE_ALARM_TRIGGERED
|
||||
]
|
||||
|
||||
SUPPORTED_PENDING_STATES = [state for state in SUPPORTED_STATES
|
||||
if state != STATE_ALARM_DISARMED]
|
||||
SUPPORTED_PENDING_STATES = [
|
||||
state for state in SUPPORTED_STATES if state != STATE_ALARM_DISARMED
|
||||
]
|
||||
|
||||
ATTR_PRE_PENDING_STATE = 'pre_pending_state'
|
||||
ATTR_POST_PENDING_STATE = 'post_pending_state'
|
||||
ATTR_PRE_PENDING_STATE = "pre_pending_state"
|
||||
ATTR_POST_PENDING_STATE = "post_pending_state"
|
||||
|
||||
|
||||
def _state_validator(config):
|
||||
|
|
@ -66,53 +83,75 @@ def _state_schema(state):
|
|||
schema = {}
|
||||
if state in SUPPORTED_PRETRIGGER_STATES:
|
||||
schema[vol.Optional(CONF_DELAY_TIME)] = vol.All(
|
||||
cv.time_period, cv.positive_timedelta)
|
||||
cv.time_period, cv.positive_timedelta
|
||||
)
|
||||
schema[vol.Optional(CONF_TRIGGER_TIME)] = vol.All(
|
||||
cv.time_period, cv.positive_timedelta)
|
||||
cv.time_period, cv.positive_timedelta
|
||||
)
|
||||
if state in SUPPORTED_PENDING_STATES:
|
||||
schema[vol.Optional(CONF_PENDING_TIME)] = vol.All(
|
||||
cv.time_period, cv.positive_timedelta)
|
||||
cv.time_period, cv.positive_timedelta
|
||||
)
|
||||
return vol.Schema(schema)
|
||||
|
||||
|
||||
PLATFORM_SCHEMA = vol.Schema(vol.All({
|
||||
vol.Required(CONF_PLATFORM): 'manual',
|
||||
PLATFORM_SCHEMA = vol.Schema(
|
||||
vol.All(
|
||||
{
|
||||
vol.Required(CONF_PLATFORM): "manual",
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_ALARM_NAME): cv.string,
|
||||
vol.Exclusive(CONF_CODE, 'code validation'): cv.string,
|
||||
vol.Exclusive(CONF_CODE_TEMPLATE, 'code validation'): cv.template,
|
||||
vol.Optional(CONF_DELAY_TIME, default=DEFAULT_DELAY_TIME):
|
||||
vol.All(cv.time_period, cv.positive_timedelta),
|
||||
vol.Optional(CONF_PENDING_TIME, default=DEFAULT_PENDING_TIME):
|
||||
vol.All(cv.time_period, cv.positive_timedelta),
|
||||
vol.Optional(CONF_TRIGGER_TIME, default=DEFAULT_TRIGGER_TIME):
|
||||
vol.All(cv.time_period, cv.positive_timedelta),
|
||||
vol.Optional(CONF_DISARM_AFTER_TRIGGER,
|
||||
default=DEFAULT_DISARM_AFTER_TRIGGER): cv.boolean,
|
||||
vol.Optional(STATE_ALARM_ARMED_AWAY, default={}):
|
||||
_state_schema(STATE_ALARM_ARMED_AWAY),
|
||||
vol.Optional(STATE_ALARM_ARMED_HOME, default={}):
|
||||
_state_schema(STATE_ALARM_ARMED_HOME),
|
||||
vol.Optional(STATE_ALARM_ARMED_NIGHT, default={}):
|
||||
_state_schema(STATE_ALARM_ARMED_NIGHT),
|
||||
vol.Optional(STATE_ALARM_ARMED_CUSTOM_BYPASS, default={}):
|
||||
_state_schema(STATE_ALARM_ARMED_CUSTOM_BYPASS),
|
||||
vol.Optional(STATE_ALARM_DISARMED, default={}):
|
||||
_state_schema(STATE_ALARM_DISARMED),
|
||||
vol.Optional(STATE_ALARM_TRIGGERED, default={}):
|
||||
_state_schema(STATE_ALARM_TRIGGERED),
|
||||
}, _state_validator))
|
||||
vol.Exclusive(CONF_CODE, "code validation"): cv.string,
|
||||
vol.Exclusive(CONF_CODE_TEMPLATE, "code validation"): cv.template,
|
||||
vol.Optional(CONF_DELAY_TIME, default=DEFAULT_DELAY_TIME): vol.All(
|
||||
cv.time_period, cv.positive_timedelta
|
||||
),
|
||||
vol.Optional(CONF_PENDING_TIME, default=DEFAULT_PENDING_TIME): vol.All(
|
||||
cv.time_period, cv.positive_timedelta
|
||||
),
|
||||
vol.Optional(CONF_TRIGGER_TIME, default=DEFAULT_TRIGGER_TIME): vol.All(
|
||||
cv.time_period, cv.positive_timedelta
|
||||
),
|
||||
vol.Optional(
|
||||
CONF_DISARM_AFTER_TRIGGER, default=DEFAULT_DISARM_AFTER_TRIGGER
|
||||
): cv.boolean,
|
||||
vol.Optional(STATE_ALARM_ARMED_AWAY, default={}): _state_schema(
|
||||
STATE_ALARM_ARMED_AWAY
|
||||
),
|
||||
vol.Optional(STATE_ALARM_ARMED_HOME, default={}): _state_schema(
|
||||
STATE_ALARM_ARMED_HOME
|
||||
),
|
||||
vol.Optional(STATE_ALARM_ARMED_NIGHT, default={}): _state_schema(
|
||||
STATE_ALARM_ARMED_NIGHT
|
||||
),
|
||||
vol.Optional(STATE_ALARM_ARMED_CUSTOM_BYPASS, default={}): _state_schema(
|
||||
STATE_ALARM_ARMED_CUSTOM_BYPASS
|
||||
),
|
||||
vol.Optional(STATE_ALARM_DISARMED, default={}): _state_schema(
|
||||
STATE_ALARM_DISARMED
|
||||
),
|
||||
vol.Optional(STATE_ALARM_TRIGGERED, default={}): _state_schema(
|
||||
STATE_ALARM_TRIGGERED
|
||||
),
|
||||
},
|
||||
_state_validator,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the manual alarm platform."""
|
||||
add_entities([ManualAlarm(
|
||||
add_entities(
|
||||
[
|
||||
ManualAlarm(
|
||||
hass,
|
||||
config[CONF_NAME],
|
||||
config.get(CONF_CODE),
|
||||
config.get(CONF_CODE_TEMPLATE),
|
||||
config.get(CONF_DISARM_AFTER_TRIGGER, DEFAULT_DISARM_AFTER_TRIGGER),
|
||||
config
|
||||
)])
|
||||
config,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class ManualAlarm(alarm.AlarmControlPanel):
|
||||
|
|
@ -127,8 +166,7 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
|||
A trigger_time of zero disables the alarm_trigger service.
|
||||
"""
|
||||
|
||||
def __init__(self, hass, name, code, code_template,
|
||||
disarm_after_trigger, config):
|
||||
def __init__(self, hass, name, code, code_template, disarm_after_trigger, config):
|
||||
"""Init the manual alarm panel."""
|
||||
self._state = STATE_ALARM_DISARMED
|
||||
self._hass = hass
|
||||
|
|
@ -144,13 +182,16 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
|||
|
||||
self._delay_time_by_state = {
|
||||
state: config[state][CONF_DELAY_TIME]
|
||||
for state in SUPPORTED_PRETRIGGER_STATES}
|
||||
for state in SUPPORTED_PRETRIGGER_STATES
|
||||
}
|
||||
self._trigger_time_by_state = {
|
||||
state: config[state][CONF_TRIGGER_TIME]
|
||||
for state in SUPPORTED_PRETRIGGER_STATES}
|
||||
for state in SUPPORTED_PRETRIGGER_STATES
|
||||
}
|
||||
self._pending_time_by_state = {
|
||||
state: config[state][CONF_PENDING_TIME]
|
||||
for state in SUPPORTED_PENDING_STATES}
|
||||
for state in SUPPORTED_PENDING_STATES
|
||||
}
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
|
|
@ -169,15 +210,17 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
|||
if self._within_pending_time(self._state):
|
||||
return STATE_ALARM_PENDING
|
||||
trigger_time = self._trigger_time_by_state[self._previous_state]
|
||||
if (self._state_ts + self._pending_time(self._state) +
|
||||
trigger_time) < dt_util.utcnow():
|
||||
if (
|
||||
self._state_ts + self._pending_time(self._state) + trigger_time
|
||||
) < dt_util.utcnow():
|
||||
if self._disarm_after_trigger:
|
||||
return STATE_ALARM_DISARMED
|
||||
self._state = self._previous_state
|
||||
return self._state
|
||||
|
||||
if self._state in SUPPORTED_PENDING_STATES and \
|
||||
self._within_pending_time(self._state):
|
||||
if self._state in SUPPORTED_PENDING_STATES and self._within_pending_time(
|
||||
self._state
|
||||
):
|
||||
return STATE_ALARM_PENDING
|
||||
|
||||
return self._state
|
||||
|
|
@ -205,9 +248,9 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
|||
"""Return one or more digits/characters."""
|
||||
if self._code is None:
|
||||
return None
|
||||
if isinstance(self._code, str) and re.search('^\\d+$', self._code):
|
||||
return 'Number'
|
||||
return 'Any'
|
||||
if isinstance(self._code, str) and re.search("^\\d+$", self._code):
|
||||
return "Number"
|
||||
return "Any"
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
|
|
@ -270,17 +313,19 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
|||
pending_time = self._pending_time(state)
|
||||
if state == STATE_ALARM_TRIGGERED:
|
||||
track_point_in_time(
|
||||
self._hass, self.async_update_ha_state,
|
||||
self._state_ts + pending_time)
|
||||
self._hass, self.async_update_ha_state, self._state_ts + pending_time
|
||||
)
|
||||
|
||||
trigger_time = self._trigger_time_by_state[self._previous_state]
|
||||
track_point_in_time(
|
||||
self._hass, self.async_update_ha_state,
|
||||
self._state_ts + pending_time + trigger_time)
|
||||
self._hass,
|
||||
self.async_update_ha_state,
|
||||
self._state_ts + pending_time + trigger_time,
|
||||
)
|
||||
elif state in SUPPORTED_PENDING_STATES and pending_time:
|
||||
track_point_in_time(
|
||||
self._hass, self.async_update_ha_state,
|
||||
self._state_ts + pending_time)
|
||||
self._hass, self.async_update_ha_state, self._state_ts + pending_time
|
||||
)
|
||||
|
||||
def _validate_code(self, code, state):
|
||||
"""Validate given code."""
|
||||
|
|
@ -289,8 +334,7 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
|||
if isinstance(self._code, str):
|
||||
alarm_code = self._code
|
||||
else:
|
||||
alarm_code = self._code.render(from_state=self._state,
|
||||
to_state=state)
|
||||
alarm_code = self._code.render(from_state=self._state, to_state=state)
|
||||
check = not alarm_code or code == alarm_code
|
||||
if not check:
|
||||
_LOGGER.warning("Invalid code given for %s", state)
|
||||
|
|
|
|||
|
|
@ -15,10 +15,20 @@ import voluptuous as vol
|
|||
import homeassistant.components.alarm_control_panel as alarm
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT,
|
||||
STATE_ALARM_DISARMED, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED,
|
||||
CONF_PLATFORM, CONF_NAME, CONF_CODE, CONF_DELAY_TIME, CONF_PENDING_TIME,
|
||||
CONF_TRIGGER_TIME, CONF_DISARM_AFTER_TRIGGER)
|
||||
STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_ARMED_NIGHT,
|
||||
STATE_ALARM_DISARMED,
|
||||
STATE_ALARM_PENDING,
|
||||
STATE_ALARM_TRIGGERED,
|
||||
CONF_PLATFORM,
|
||||
CONF_NAME,
|
||||
CONF_CODE,
|
||||
CONF_DELAY_TIME,
|
||||
CONF_PENDING_TIME,
|
||||
CONF_TRIGGER_TIME,
|
||||
CONF_DISARM_AFTER_TRIGGER,
|
||||
)
|
||||
from homeassistant.components import mqtt
|
||||
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
|
|
@ -29,35 +39,41 @@ from homeassistant.helpers.event import track_point_in_time
|
|||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_CODE_TEMPLATE = 'code_template'
|
||||
CONF_CODE_TEMPLATE = "code_template"
|
||||
|
||||
CONF_PAYLOAD_DISARM = 'payload_disarm'
|
||||
CONF_PAYLOAD_ARM_HOME = 'payload_arm_home'
|
||||
CONF_PAYLOAD_ARM_AWAY = 'payload_arm_away'
|
||||
CONF_PAYLOAD_ARM_NIGHT = 'payload_arm_night'
|
||||
CONF_PAYLOAD_DISARM = "payload_disarm"
|
||||
CONF_PAYLOAD_ARM_HOME = "payload_arm_home"
|
||||
CONF_PAYLOAD_ARM_AWAY = "payload_arm_away"
|
||||
CONF_PAYLOAD_ARM_NIGHT = "payload_arm_night"
|
||||
|
||||
DEFAULT_ALARM_NAME = 'HA Alarm'
|
||||
DEFAULT_ALARM_NAME = "HA Alarm"
|
||||
DEFAULT_DELAY_TIME = datetime.timedelta(seconds=0)
|
||||
DEFAULT_PENDING_TIME = datetime.timedelta(seconds=60)
|
||||
DEFAULT_TRIGGER_TIME = datetime.timedelta(seconds=120)
|
||||
DEFAULT_DISARM_AFTER_TRIGGER = False
|
||||
DEFAULT_ARM_AWAY = 'ARM_AWAY'
|
||||
DEFAULT_ARM_HOME = 'ARM_HOME'
|
||||
DEFAULT_ARM_NIGHT = 'ARM_NIGHT'
|
||||
DEFAULT_DISARM = 'DISARM'
|
||||
DEFAULT_ARM_AWAY = "ARM_AWAY"
|
||||
DEFAULT_ARM_HOME = "ARM_HOME"
|
||||
DEFAULT_ARM_NIGHT = "ARM_NIGHT"
|
||||
DEFAULT_DISARM = "DISARM"
|
||||
|
||||
SUPPORTED_STATES = [STATE_ALARM_DISARMED, STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT,
|
||||
STATE_ALARM_TRIGGERED]
|
||||
SUPPORTED_STATES = [
|
||||
STATE_ALARM_DISARMED,
|
||||
STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_ARMED_NIGHT,
|
||||
STATE_ALARM_TRIGGERED,
|
||||
]
|
||||
|
||||
SUPPORTED_PRETRIGGER_STATES = [state for state in SUPPORTED_STATES
|
||||
if state != STATE_ALARM_TRIGGERED]
|
||||
SUPPORTED_PRETRIGGER_STATES = [
|
||||
state for state in SUPPORTED_STATES if state != STATE_ALARM_TRIGGERED
|
||||
]
|
||||
|
||||
SUPPORTED_PENDING_STATES = [state for state in SUPPORTED_STATES
|
||||
if state != STATE_ALARM_DISARMED]
|
||||
SUPPORTED_PENDING_STATES = [
|
||||
state for state in SUPPORTED_STATES if state != STATE_ALARM_DISARMED
|
||||
]
|
||||
|
||||
ATTR_PRE_PENDING_STATE = 'pre_pending_state'
|
||||
ATTR_POST_PENDING_STATE = 'post_pending_state'
|
||||
ATTR_PRE_PENDING_STATE = "pre_pending_state"
|
||||
ATTR_POST_PENDING_STATE = "post_pending_state"
|
||||
|
||||
|
||||
def _state_validator(config):
|
||||
|
|
@ -80,52 +96,79 @@ def _state_schema(state):
|
|||
schema = {}
|
||||
if state in SUPPORTED_PRETRIGGER_STATES:
|
||||
schema[vol.Optional(CONF_DELAY_TIME)] = vol.All(
|
||||
cv.time_period, cv.positive_timedelta)
|
||||
cv.time_period, cv.positive_timedelta
|
||||
)
|
||||
schema[vol.Optional(CONF_TRIGGER_TIME)] = vol.All(
|
||||
cv.time_period, cv.positive_timedelta)
|
||||
cv.time_period, cv.positive_timedelta
|
||||
)
|
||||
if state in SUPPORTED_PENDING_STATES:
|
||||
schema[vol.Optional(CONF_PENDING_TIME)] = vol.All(
|
||||
cv.time_period, cv.positive_timedelta)
|
||||
cv.time_period, cv.positive_timedelta
|
||||
)
|
||||
return vol.Schema(schema)
|
||||
|
||||
|
||||
DEPENDENCIES = ['mqtt']
|
||||
DEPENDENCIES = ["mqtt"]
|
||||
|
||||
PLATFORM_SCHEMA = vol.Schema(vol.All(mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_PLATFORM): 'manual_mqtt',
|
||||
PLATFORM_SCHEMA = vol.Schema(
|
||||
vol.All(
|
||||
mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_PLATFORM): "manual_mqtt",
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_ALARM_NAME): cv.string,
|
||||
vol.Exclusive(CONF_CODE, 'code validation'): cv.string,
|
||||
vol.Exclusive(CONF_CODE_TEMPLATE, 'code validation'): cv.template,
|
||||
vol.Optional(CONF_DELAY_TIME, default=DEFAULT_DELAY_TIME):
|
||||
vol.All(cv.time_period, cv.positive_timedelta),
|
||||
vol.Optional(CONF_PENDING_TIME, default=DEFAULT_PENDING_TIME):
|
||||
vol.All(cv.time_period, cv.positive_timedelta),
|
||||
vol.Optional(CONF_TRIGGER_TIME, default=DEFAULT_TRIGGER_TIME):
|
||||
vol.All(cv.time_period, cv.positive_timedelta),
|
||||
vol.Optional(CONF_DISARM_AFTER_TRIGGER,
|
||||
default=DEFAULT_DISARM_AFTER_TRIGGER): cv.boolean,
|
||||
vol.Optional(STATE_ALARM_ARMED_AWAY, default={}):
|
||||
_state_schema(STATE_ALARM_ARMED_AWAY),
|
||||
vol.Optional(STATE_ALARM_ARMED_HOME, default={}):
|
||||
_state_schema(STATE_ALARM_ARMED_HOME),
|
||||
vol.Optional(STATE_ALARM_ARMED_NIGHT, default={}):
|
||||
_state_schema(STATE_ALARM_ARMED_NIGHT),
|
||||
vol.Optional(STATE_ALARM_DISARMED, default={}):
|
||||
_state_schema(STATE_ALARM_DISARMED),
|
||||
vol.Optional(STATE_ALARM_TRIGGERED, default={}):
|
||||
_state_schema(STATE_ALARM_TRIGGERED),
|
||||
vol.Exclusive(CONF_CODE, "code validation"): cv.string,
|
||||
vol.Exclusive(CONF_CODE_TEMPLATE, "code validation"): cv.template,
|
||||
vol.Optional(CONF_DELAY_TIME, default=DEFAULT_DELAY_TIME): vol.All(
|
||||
cv.time_period, cv.positive_timedelta
|
||||
),
|
||||
vol.Optional(CONF_PENDING_TIME, default=DEFAULT_PENDING_TIME): vol.All(
|
||||
cv.time_period, cv.positive_timedelta
|
||||
),
|
||||
vol.Optional(CONF_TRIGGER_TIME, default=DEFAULT_TRIGGER_TIME): vol.All(
|
||||
cv.time_period, cv.positive_timedelta
|
||||
),
|
||||
vol.Optional(
|
||||
CONF_DISARM_AFTER_TRIGGER, default=DEFAULT_DISARM_AFTER_TRIGGER
|
||||
): cv.boolean,
|
||||
vol.Optional(STATE_ALARM_ARMED_AWAY, default={}): _state_schema(
|
||||
STATE_ALARM_ARMED_AWAY
|
||||
),
|
||||
vol.Optional(STATE_ALARM_ARMED_HOME, default={}): _state_schema(
|
||||
STATE_ALARM_ARMED_HOME
|
||||
),
|
||||
vol.Optional(STATE_ALARM_ARMED_NIGHT, default={}): _state_schema(
|
||||
STATE_ALARM_ARMED_NIGHT
|
||||
),
|
||||
vol.Optional(STATE_ALARM_DISARMED, default={}): _state_schema(
|
||||
STATE_ALARM_DISARMED
|
||||
),
|
||||
vol.Optional(STATE_ALARM_TRIGGERED, default={}): _state_schema(
|
||||
STATE_ALARM_TRIGGERED
|
||||
),
|
||||
vol.Required(mqtt.CONF_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||
vol.Required(mqtt.CONF_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
||||
vol.Optional(CONF_PAYLOAD_ARM_AWAY, default=DEFAULT_ARM_AWAY): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_ARM_HOME, default=DEFAULT_ARM_HOME): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_ARM_NIGHT, default=DEFAULT_ARM_NIGHT): cv.string,
|
||||
vol.Optional(
|
||||
CONF_PAYLOAD_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))
|
||||
}
|
||||
),
|
||||
_state_validator,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the manual MQTT alarm platform."""
|
||||
add_entities([ManualMQTTAlarm(
|
||||
add_entities(
|
||||
[
|
||||
ManualMQTTAlarm(
|
||||
hass,
|
||||
config[CONF_NAME],
|
||||
config.get(CONF_CODE),
|
||||
|
|
@ -138,7 +181,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||
config.get(CONF_PAYLOAD_ARM_HOME),
|
||||
config.get(CONF_PAYLOAD_ARM_AWAY),
|
||||
config.get(CONF_PAYLOAD_ARM_NIGHT),
|
||||
config)])
|
||||
config,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class ManualMQTTAlarm(alarm.AlarmControlPanel):
|
||||
|
|
@ -153,10 +199,22 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
|
|||
A trigger_time of zero disables the alarm_trigger service.
|
||||
"""
|
||||
|
||||
def __init__(self, hass, name, code, code_template, disarm_after_trigger,
|
||||
state_topic, command_topic, qos, payload_disarm,
|
||||
payload_arm_home, payload_arm_away, payload_arm_night,
|
||||
config):
|
||||
def __init__(
|
||||
self,
|
||||
hass,
|
||||
name,
|
||||
code,
|
||||
code_template,
|
||||
disarm_after_trigger,
|
||||
state_topic,
|
||||
command_topic,
|
||||
qos,
|
||||
payload_disarm,
|
||||
payload_arm_home,
|
||||
payload_arm_away,
|
||||
payload_arm_night,
|
||||
config,
|
||||
):
|
||||
"""Init the manual MQTT alarm panel."""
|
||||
self._state = STATE_ALARM_DISARMED
|
||||
self._hass = hass
|
||||
|
|
@ -172,13 +230,16 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
|
|||
|
||||
self._delay_time_by_state = {
|
||||
state: config[state][CONF_DELAY_TIME]
|
||||
for state in SUPPORTED_PRETRIGGER_STATES}
|
||||
for state in SUPPORTED_PRETRIGGER_STATES
|
||||
}
|
||||
self._trigger_time_by_state = {
|
||||
state: config[state][CONF_TRIGGER_TIME]
|
||||
for state in SUPPORTED_PRETRIGGER_STATES}
|
||||
for state in SUPPORTED_PRETRIGGER_STATES
|
||||
}
|
||||
self._pending_time_by_state = {
|
||||
state: config[state][CONF_PENDING_TIME]
|
||||
for state in SUPPORTED_PENDING_STATES}
|
||||
for state in SUPPORTED_PENDING_STATES
|
||||
}
|
||||
|
||||
self._state_topic = state_topic
|
||||
self._command_topic = command_topic
|
||||
|
|
@ -205,15 +266,17 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
|
|||
if self._within_pending_time(self._state):
|
||||
return STATE_ALARM_PENDING
|
||||
trigger_time = self._trigger_time_by_state[self._previous_state]
|
||||
if (self._state_ts + self._pending_time(self._state) +
|
||||
trigger_time) < dt_util.utcnow():
|
||||
if (
|
||||
self._state_ts + self._pending_time(self._state) + trigger_time
|
||||
) < dt_util.utcnow():
|
||||
if self._disarm_after_trigger:
|
||||
return STATE_ALARM_DISARMED
|
||||
self._state = self._previous_state
|
||||
return self._state
|
||||
|
||||
if self._state in SUPPORTED_PENDING_STATES and \
|
||||
self._within_pending_time(self._state):
|
||||
if self._state in SUPPORTED_PENDING_STATES and self._within_pending_time(
|
||||
self._state
|
||||
):
|
||||
return STATE_ALARM_PENDING
|
||||
|
||||
return self._state
|
||||
|
|
@ -241,9 +304,9 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
|
|||
"""Return one or more digits/characters."""
|
||||
if self._code is None:
|
||||
return None
|
||||
if isinstance(self._code, str) and re.search('^\\d+$', self._code):
|
||||
return 'Number'
|
||||
return 'Any'
|
||||
if isinstance(self._code, str) and re.search("^\\d+$", self._code):
|
||||
return "Number"
|
||||
return "Any"
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
|
|
@ -299,17 +362,19 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
|
|||
pending_time = self._pending_time(state)
|
||||
if state == STATE_ALARM_TRIGGERED:
|
||||
track_point_in_time(
|
||||
self._hass, self.async_update_ha_state,
|
||||
self._state_ts + pending_time)
|
||||
self._hass, self.async_update_ha_state, self._state_ts + pending_time
|
||||
)
|
||||
|
||||
trigger_time = self._trigger_time_by_state[self._previous_state]
|
||||
track_point_in_time(
|
||||
self._hass, self.async_update_ha_state,
|
||||
self._state_ts + pending_time + trigger_time)
|
||||
self._hass,
|
||||
self.async_update_ha_state,
|
||||
self._state_ts + pending_time + trigger_time,
|
||||
)
|
||||
elif state in SUPPORTED_PENDING_STATES and pending_time:
|
||||
track_point_in_time(
|
||||
self._hass, self.async_update_ha_state,
|
||||
self._state_ts + pending_time)
|
||||
self._hass, self.async_update_ha_state, self._state_ts + pending_time
|
||||
)
|
||||
|
||||
def _validate_code(self, code, state):
|
||||
"""Validate given code."""
|
||||
|
|
@ -318,8 +383,7 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
|
|||
if isinstance(self._code, str):
|
||||
alarm_code = self._code
|
||||
else:
|
||||
alarm_code = self._code.render(from_state=self._state,
|
||||
to_state=state)
|
||||
alarm_code = self._code.render(from_state=self._state, to_state=state)
|
||||
check = not alarm_code or code == alarm_code
|
||||
if not check:
|
||||
_LOGGER.warning("Invalid code given for %s", state)
|
||||
|
|
@ -361,10 +425,12 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
|
|||
return
|
||||
|
||||
return mqtt.async_subscribe(
|
||||
self.hass, self._command_topic, message_received, self._qos)
|
||||
self.hass, self._command_topic, message_received, self._qos
|
||||
)
|
||||
|
||||
@asyncio.coroutine
|
||||
def _async_state_changed_listener(self, entity_id, old_state, new_state):
|
||||
"""Publish state change to MQTT."""
|
||||
mqtt.async_publish(
|
||||
self.hass, self._state_topic, new_state.state, self._qos, True)
|
||||
self.hass, self._state_topic, new_state.state, self._qos, True
|
||||
)
|
||||
|
|
|
|||
|
|
@ -14,28 +14,41 @@ from homeassistant.core import callback
|
|||
import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.components import mqtt
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
|
||||
STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, STATE_UNKNOWN,
|
||||
CONF_NAME, CONF_CODE)
|
||||
STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_DISARMED,
|
||||
STATE_ALARM_PENDING,
|
||||
STATE_ALARM_TRIGGERED,
|
||||
STATE_UNKNOWN,
|
||||
CONF_NAME,
|
||||
CONF_CODE,
|
||||
)
|
||||
from homeassistant.components.mqtt import (
|
||||
CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_COMMAND_TOPIC,
|
||||
CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS,
|
||||
CONF_RETAIN, MqttAvailability)
|
||||
CONF_AVAILABILITY_TOPIC,
|
||||
CONF_STATE_TOPIC,
|
||||
CONF_COMMAND_TOPIC,
|
||||
CONF_PAYLOAD_AVAILABLE,
|
||||
CONF_PAYLOAD_NOT_AVAILABLE,
|
||||
CONF_QOS,
|
||||
CONF_RETAIN,
|
||||
MqttAvailability,
|
||||
)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_PAYLOAD_DISARM = 'payload_disarm'
|
||||
CONF_PAYLOAD_ARM_HOME = 'payload_arm_home'
|
||||
CONF_PAYLOAD_ARM_AWAY = 'payload_arm_away'
|
||||
CONF_PAYLOAD_DISARM = "payload_disarm"
|
||||
CONF_PAYLOAD_ARM_HOME = "payload_arm_home"
|
||||
CONF_PAYLOAD_ARM_AWAY = "payload_arm_away"
|
||||
|
||||
DEFAULT_ARM_AWAY = 'ARM_AWAY'
|
||||
DEFAULT_ARM_HOME = 'ARM_HOME'
|
||||
DEFAULT_DISARM = 'DISARM'
|
||||
DEFAULT_NAME = 'MQTT Alarm'
|
||||
DEPENDENCIES = ['mqtt']
|
||||
DEFAULT_ARM_AWAY = "ARM_AWAY"
|
||||
DEFAULT_ARM_HOME = "ARM_HOME"
|
||||
DEFAULT_DISARM = "DISARM"
|
||||
DEFAULT_NAME = "MQTT Alarm"
|
||||
DEPENDENCIES = ["mqtt"]
|
||||
|
||||
PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
|
||||
PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||
vol.Required(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
||||
vol.Optional(CONF_CODE): cv.string,
|
||||
|
|
@ -43,17 +56,19 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
|
|||
vol.Optional(CONF_PAYLOAD_ARM_AWAY, default=DEFAULT_ARM_AWAY): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_ARM_HOME, default=DEFAULT_ARM_HOME): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_DISARM, default=DEFAULT_DISARM): cv.string,
|
||||
}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema)
|
||||
}
|
||||
).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||
"""Set up the MQTT Alarm Control Panel platform."""
|
||||
if discovery_info is not None:
|
||||
config = PLATFORM_SCHEMA(discovery_info)
|
||||
|
||||
async_add_entities([MqttAlarm(
|
||||
async_add_entities(
|
||||
[
|
||||
MqttAlarm(
|
||||
config.get(CONF_NAME),
|
||||
config.get(CONF_STATE_TOPIC),
|
||||
config.get(CONF_COMMAND_TOPIC),
|
||||
|
|
@ -65,18 +80,34 @@ def async_setup_platform(hass, config, async_add_entities,
|
|||
config.get(CONF_CODE),
|
||||
config.get(CONF_AVAILABILITY_TOPIC),
|
||||
config.get(CONF_PAYLOAD_AVAILABLE),
|
||||
config.get(CONF_PAYLOAD_NOT_AVAILABLE))])
|
||||
config.get(CONF_PAYLOAD_NOT_AVAILABLE),
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel):
|
||||
"""Representation of a MQTT alarm status."""
|
||||
|
||||
def __init__(self, name, state_topic, command_topic, qos, retain,
|
||||
payload_disarm, payload_arm_home, payload_arm_away, code,
|
||||
availability_topic, payload_available, payload_not_available):
|
||||
def __init__(
|
||||
self,
|
||||
name,
|
||||
state_topic,
|
||||
command_topic,
|
||||
qos,
|
||||
retain,
|
||||
payload_disarm,
|
||||
payload_arm_home,
|
||||
payload_arm_away,
|
||||
code,
|
||||
availability_topic,
|
||||
payload_available,
|
||||
payload_not_available,
|
||||
):
|
||||
"""Init the MQTT Alarm Control Panel."""
|
||||
super().__init__(availability_topic, qos, payload_available,
|
||||
payload_not_available)
|
||||
super().__init__(
|
||||
availability_topic, qos, payload_available, payload_not_available
|
||||
)
|
||||
self._state = STATE_UNKNOWN
|
||||
self._name = name
|
||||
self._state_topic = state_topic
|
||||
|
|
@ -96,16 +127,21 @@ class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel):
|
|||
@callback
|
||||
def message_received(topic, payload, qos):
|
||||
"""Run when new MQTT message has been received."""
|
||||
if payload not in (STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_PENDING,
|
||||
STATE_ALARM_TRIGGERED):
|
||||
if payload not in (
|
||||
STATE_ALARM_DISARMED,
|
||||
STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_PENDING,
|
||||
STATE_ALARM_TRIGGERED,
|
||||
):
|
||||
_LOGGER.warning("Received unexpected payload: %s", payload)
|
||||
return
|
||||
self._state = payload
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
yield from mqtt.async_subscribe(
|
||||
self.hass, self._state_topic, message_received, self._qos)
|
||||
self.hass, self._state_topic, message_received, self._qos
|
||||
)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
|
|
@ -127,9 +163,9 @@ class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel):
|
|||
"""Return one or more digits/characters."""
|
||||
if self._code is None:
|
||||
return None
|
||||
if isinstance(self._code, str) and re.search('^\\d+$', self._code):
|
||||
return 'Number'
|
||||
return 'Any'
|
||||
if isinstance(self._code, str) and re.search("^\\d+$", self._code):
|
||||
return "Number"
|
||||
return "Any"
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_disarm(self, code=None):
|
||||
|
|
@ -137,11 +173,15 @@ class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel):
|
|||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
if not self._validate_code(code, 'disarming'):
|
||||
if not self._validate_code(code, "disarming"):
|
||||
return
|
||||
mqtt.async_publish(
|
||||
self.hass, self._command_topic, self._payload_disarm, self._qos,
|
||||
self._retain)
|
||||
self.hass,
|
||||
self._command_topic,
|
||||
self._payload_disarm,
|
||||
self._qos,
|
||||
self._retain,
|
||||
)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_arm_home(self, code=None):
|
||||
|
|
@ -149,11 +189,15 @@ class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel):
|
|||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
if not self._validate_code(code, 'arming home'):
|
||||
if not self._validate_code(code, "arming home"):
|
||||
return
|
||||
mqtt.async_publish(
|
||||
self.hass, self._command_topic, self._payload_arm_home, self._qos,
|
||||
self._retain)
|
||||
self.hass,
|
||||
self._command_topic,
|
||||
self._payload_arm_home,
|
||||
self._qos,
|
||||
self._retain,
|
||||
)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_arm_away(self, code=None):
|
||||
|
|
@ -161,15 +205,19 @@ class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel):
|
|||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
if not self._validate_code(code, 'arming away'):
|
||||
if not self._validate_code(code, "arming away"):
|
||||
return
|
||||
mqtt.async_publish(
|
||||
self.hass, self._command_topic, self._payload_arm_away, self._qos,
|
||||
self._retain)
|
||||
self.hass,
|
||||
self._command_topic,
|
||||
self._payload_arm_away,
|
||||
self._qos,
|
||||
self._retain,
|
||||
)
|
||||
|
||||
def _validate_code(self, code, state):
|
||||
"""Validate given code."""
|
||||
check = self._code is None or code == self._code
|
||||
if not check:
|
||||
_LOGGER.warning('Wrong code entered for %s', state)
|
||||
_LOGGER.warning("Wrong code entered for %s", state)
|
||||
return check
|
||||
|
|
|
|||
|
|
@ -12,23 +12,31 @@ import voluptuous as vol
|
|||
import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, CONF_NAME, CONF_PORT, STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_UNKNOWN)
|
||||
CONF_HOST,
|
||||
CONF_NAME,
|
||||
CONF_PORT,
|
||||
STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_DISARMED,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pynx584==0.4']
|
||||
REQUIREMENTS = ["pynx584==0.4"]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_HOST = 'localhost'
|
||||
DEFAULT_NAME = 'NX584'
|
||||
DEFAULT_HOST = "localhost"
|
||||
DEFAULT_NAME = "NX584"
|
||||
DEFAULT_PORT = 5007
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
|
|
@ -37,7 +45,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||
host = config.get(CONF_HOST)
|
||||
port = config.get(CONF_PORT)
|
||||
|
||||
url = 'http://{}:{}'.format(host, port)
|
||||
url = "http://{}:{}".format(host, port)
|
||||
|
||||
try:
|
||||
add_entities([NX584Alarm(hass, url, name)])
|
||||
|
|
@ -52,6 +60,7 @@ class NX584Alarm(alarm.AlarmControlPanel):
|
|||
def __init__(self, hass, url, name):
|
||||
"""Init the nx584 alarm panel."""
|
||||
from nx584 import client
|
||||
|
||||
self._hass = hass
|
||||
self._name = name
|
||||
self._url = url
|
||||
|
|
@ -70,7 +79,7 @@ class NX584Alarm(alarm.AlarmControlPanel):
|
|||
@property
|
||||
def code_format(self):
|
||||
"""Return one or more digits/characters."""
|
||||
return 'Number'
|
||||
return "Number"
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
|
|
@ -83,8 +92,10 @@ class NX584Alarm(alarm.AlarmControlPanel):
|
|||
part = self._alarm.list_partitions()[0]
|
||||
zones = self._alarm.list_zones()
|
||||
except requests.exceptions.ConnectionError as ex:
|
||||
_LOGGER.error("Unable to connect to %(host)s: %(reason)s",
|
||||
dict(host=self._url, reason=ex))
|
||||
_LOGGER.error(
|
||||
"Unable to connect to %(host)s: %(reason)s",
|
||||
dict(host=self._url, reason=ex),
|
||||
)
|
||||
self._state = STATE_UNKNOWN
|
||||
zones = []
|
||||
except IndexError:
|
||||
|
|
@ -94,13 +105,15 @@ class NX584Alarm(alarm.AlarmControlPanel):
|
|||
|
||||
bypassed = False
|
||||
for zone in zones:
|
||||
if zone['bypassed']:
|
||||
_LOGGER.debug("Zone %(zone)s is bypassed, assuming HOME",
|
||||
dict(zone=zone['number']))
|
||||
if zone["bypassed"]:
|
||||
_LOGGER.debug(
|
||||
"Zone %(zone)s is bypassed, assuming HOME",
|
||||
dict(zone=zone["number"]),
|
||||
)
|
||||
bypassed = True
|
||||
break
|
||||
|
||||
if not part['armed']:
|
||||
if not part["armed"]:
|
||||
self._state = STATE_ALARM_DISARMED
|
||||
elif bypassed:
|
||||
self._state = STATE_ALARM_ARMED_HOME
|
||||
|
|
@ -113,8 +126,8 @@ class NX584Alarm(alarm.AlarmControlPanel):
|
|||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
self._alarm.arm('stay')
|
||||
self._alarm.arm("stay")
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
self._alarm.arm('exit')
|
||||
self._alarm.arm("exit")
|
||||
|
|
|
|||
|
|
@ -9,24 +9,27 @@ import logging
|
|||
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.components.satel_integra import (
|
||||
CONF_ARM_HOME_MODE, DATA_SATEL, SIGNAL_PANEL_MESSAGE)
|
||||
CONF_ARM_HOME_MODE,
|
||||
DATA_SATEL,
|
||||
SIGNAL_PANEL_MESSAGE,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['satel_integra']
|
||||
DEPENDENCIES = ["satel_integra"]
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||
"""Set up for Satel Integra alarm panels."""
|
||||
if not discovery_info:
|
||||
return
|
||||
|
||||
device = SatelIntegraAlarmPanel(
|
||||
"Alarm Panel", discovery_info.get(CONF_ARM_HOME_MODE))
|
||||
"Alarm Panel", discovery_info.get(CONF_ARM_HOME_MODE)
|
||||
)
|
||||
async_add_entities([device])
|
||||
|
||||
|
||||
|
|
@ -43,7 +46,8 @@ class SatelIntegraAlarmPanel(alarm.AlarmControlPanel):
|
|||
def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
async_dispatcher_connect(
|
||||
self.hass, SIGNAL_PANEL_MESSAGE, self._message_callback)
|
||||
self.hass, SIGNAL_PANEL_MESSAGE, self._message_callback
|
||||
)
|
||||
|
||||
@callback
|
||||
def _message_callback(self, message):
|
||||
|
|
@ -67,7 +71,7 @@ class SatelIntegraAlarmPanel(alarm.AlarmControlPanel):
|
|||
@property
|
||||
def code_format(self):
|
||||
"""Return the regex for code format or None if no code is required."""
|
||||
return 'Number'
|
||||
return "Number"
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
|
|
@ -90,5 +94,4 @@ class SatelIntegraAlarmPanel(alarm.AlarmControlPanel):
|
|||
def async_alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
if code:
|
||||
yield from self.hass.data[DATA_SATEL].arm(
|
||||
code, self._arm_home_mode)
|
||||
yield from self.hass.data[DATA_SATEL].arm(code, self._arm_home_mode)
|
||||
|
|
|
|||
|
|
@ -10,33 +10,44 @@ import re
|
|||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.alarm_control_panel import (
|
||||
PLATFORM_SCHEMA, AlarmControlPanel)
|
||||
PLATFORM_SCHEMA,
|
||||
AlarmControlPanel,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONF_CODE, CONF_NAME, CONF_PASSWORD, CONF_USERNAME,
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_DISARMED, STATE_UNKNOWN)
|
||||
CONF_CODE,
|
||||
CONF_NAME,
|
||||
CONF_PASSWORD,
|
||||
CONF_USERNAME,
|
||||
STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_DISARMED,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['simplisafe-python==2.0.2']
|
||||
REQUIREMENTS = ["simplisafe-python==2.0.2"]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = 'SimpliSafe'
|
||||
DEFAULT_NAME = "SimpliSafe"
|
||||
|
||||
ATTR_ALARM_ACTIVE = "alarm_active"
|
||||
ATTR_TEMPERATURE = "temperature"
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Optional(CONF_CODE): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the SimpliSafe platform."""
|
||||
from simplipy.api import SimpliSafeApiInterface, SimpliSafeAPIException
|
||||
|
||||
name = config.get(CONF_NAME)
|
||||
code = config.get(CONF_CODE)
|
||||
username = config.get(CONF_USERNAME)
|
||||
|
|
@ -75,27 +86,30 @@ class SimpliSafeAlarm(AlarmControlPanel):
|
|||
"""Return the name of the device."""
|
||||
if self._name is not None:
|
||||
return self._name
|
||||
return 'Alarm {}'.format(self.simplisafe.location_id)
|
||||
return "Alarm {}".format(self.simplisafe.location_id)
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
"""Return one or more digits/characters."""
|
||||
if self._code is None:
|
||||
return None
|
||||
if isinstance(self._code, str) and re.search('^\\d+$', self._code):
|
||||
return 'Number'
|
||||
return 'Any'
|
||||
if isinstance(self._code, str) and re.search("^\\d+$", self._code):
|
||||
return "Number"
|
||||
return "Any"
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
status = self.simplisafe.state
|
||||
if status.lower() == 'off':
|
||||
if status.lower() == "off":
|
||||
state = STATE_ALARM_DISARMED
|
||||
elif status.lower() == 'home' or status.lower() == 'home_count':
|
||||
elif status.lower() == "home" or status.lower() == "home_count":
|
||||
state = STATE_ALARM_ARMED_HOME
|
||||
elif (status.lower() == 'away' or status.lower() == 'exitDelay' or
|
||||
status.lower() == 'away_count'):
|
||||
elif (
|
||||
status.lower() == "away"
|
||||
or status.lower() == "exitDelay"
|
||||
or status.lower() == "away_count"
|
||||
):
|
||||
state = STATE_ALARM_ARMED_AWAY
|
||||
else:
|
||||
state = STATE_UNKNOWN
|
||||
|
|
@ -118,23 +132,23 @@ class SimpliSafeAlarm(AlarmControlPanel):
|
|||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
if not self._validate_code(code, 'disarming'):
|
||||
if not self._validate_code(code, "disarming"):
|
||||
return
|
||||
self.simplisafe.set_state('off')
|
||||
self.simplisafe.set_state("off")
|
||||
_LOGGER.info("SimpliSafe alarm disarming")
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
if not self._validate_code(code, 'arming home'):
|
||||
if not self._validate_code(code, "arming home"):
|
||||
return
|
||||
self.simplisafe.set_state('home')
|
||||
self.simplisafe.set_state("home")
|
||||
_LOGGER.info("SimpliSafe alarm arming home")
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
if not self._validate_code(code, 'arming away'):
|
||||
if not self._validate_code(code, "arming away"):
|
||||
return
|
||||
self.simplisafe.set_state('away')
|
||||
self.simplisafe.set_state("away")
|
||||
_LOGGER.info("SimpliSafe alarm arming away")
|
||||
|
||||
def _validate_code(self, code, state):
|
||||
|
|
|
|||
|
|
@ -9,17 +9,24 @@ import logging
|
|||
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.components.spc import (
|
||||
ATTR_DISCOVER_AREAS, DATA_API, DATA_REGISTRY, SpcWebGateway)
|
||||
ATTR_DISCOVER_AREAS,
|
||||
DATA_API,
|
||||
DATA_REGISTRY,
|
||||
SpcWebGateway,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
|
||||
STATE_UNKNOWN)
|
||||
STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_DISARMED,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SPC_AREA_MODE_TO_STATE = {
|
||||
'0': STATE_ALARM_DISARMED,
|
||||
'1': STATE_ALARM_ARMED_HOME,
|
||||
'3': STATE_ALARM_ARMED_AWAY,
|
||||
"0": STATE_ALARM_DISARMED,
|
||||
"1": STATE_ALARM_ARMED_HOME,
|
||||
"3": STATE_ALARM_ARMED_AWAY,
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -29,16 +36,13 @@ def _get_alarm_state(spc_mode):
|
|||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||
"""Set up the SPC alarm control panel platform."""
|
||||
if (discovery_info is None or
|
||||
discovery_info[ATTR_DISCOVER_AREAS] is None):
|
||||
if discovery_info is None or discovery_info[ATTR_DISCOVER_AREAS] is None:
|
||||
return
|
||||
|
||||
api = hass.data[DATA_API]
|
||||
devices = [SpcAlarm(api, area)
|
||||
for area in discovery_info[ATTR_DISCOVER_AREAS]]
|
||||
devices = [SpcAlarm(api, area) for area in discovery_info[ATTR_DISCOVER_AREAS]]
|
||||
|
||||
async_add_entities(devices)
|
||||
|
||||
|
|
@ -48,26 +52,25 @@ class SpcAlarm(alarm.AlarmControlPanel):
|
|||
|
||||
def __init__(self, api, area):
|
||||
"""Initialize the SPC alarm panel."""
|
||||
self._area_id = area['id']
|
||||
self._name = area['name']
|
||||
self._state = _get_alarm_state(area['mode'])
|
||||
self._area_id = area["id"]
|
||||
self._name = area["name"]
|
||||
self._state = _get_alarm_state(area["mode"])
|
||||
if self._state == STATE_ALARM_DISARMED:
|
||||
self._changed_by = area.get('last_unset_user_name', 'unknown')
|
||||
self._changed_by = area.get("last_unset_user_name", "unknown")
|
||||
else:
|
||||
self._changed_by = area.get('last_set_user_name', 'unknown')
|
||||
self._changed_by = area.get("last_set_user_name", "unknown")
|
||||
self._api = api
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Call for adding new entities."""
|
||||
self.hass.data[DATA_REGISTRY].register_alarm_device(
|
||||
self._area_id, self)
|
||||
self.hass.data[DATA_REGISTRY].register_alarm_device(self._area_id, self)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update_from_spc(self, state, extra):
|
||||
"""Update the alarm panel with a new state."""
|
||||
self._state = state
|
||||
self._changed_by = extra.get('changed_by', 'unknown')
|
||||
self._changed_by = extra.get("changed_by", "unknown")
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
|
|
@ -94,16 +97,19 @@ class SpcAlarm(alarm.AlarmControlPanel):
|
|||
def async_alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
yield from self._api.send_area_command(
|
||||
self._area_id, SpcWebGateway.AREA_COMMAND_UNSET)
|
||||
self._area_id, SpcWebGateway.AREA_COMMAND_UNSET
|
||||
)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
yield from self._api.send_area_command(
|
||||
self._area_id, SpcWebGateway.AREA_COMMAND_PART_SET)
|
||||
self._area_id, SpcWebGateway.AREA_COMMAND_PART_SET
|
||||
)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
yield from self._api.send_area_command(
|
||||
self._area_id, SpcWebGateway.AREA_COMMAND_SET)
|
||||
self._area_id, SpcWebGateway.AREA_COMMAND_SET
|
||||
)
|
||||
|
|
|
|||
|
|
@ -12,23 +12,33 @@ import homeassistant.helpers.config_validation as cv
|
|||
import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
|
||||
from homeassistant.const import (
|
||||
CONF_PASSWORD, CONF_USERNAME, STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED,
|
||||
STATE_ALARM_ARMING, STATE_ALARM_DISARMING, STATE_UNKNOWN, CONF_NAME,
|
||||
STATE_ALARM_ARMED_CUSTOM_BYPASS)
|
||||
CONF_PASSWORD,
|
||||
CONF_USERNAME,
|
||||
STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_ARMED_NIGHT,
|
||||
STATE_ALARM_DISARMED,
|
||||
STATE_ALARM_ARMING,
|
||||
STATE_ALARM_DISARMING,
|
||||
STATE_UNKNOWN,
|
||||
CONF_NAME,
|
||||
STATE_ALARM_ARMED_CUSTOM_BYPASS,
|
||||
)
|
||||
|
||||
|
||||
REQUIREMENTS = ['total_connect_client==0.18']
|
||||
REQUIREMENTS = ["total_connect_client==0.18"]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = 'Total Connect'
|
||||
DEFAULT_NAME = "Total Connect"
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
|
|
@ -53,8 +63,7 @@ class TotalConnect(alarm.AlarmControlPanel):
|
|||
self._username = username
|
||||
self._password = password
|
||||
self._state = STATE_UNKNOWN
|
||||
self._client = TotalConnectClient.TotalConnectClient(
|
||||
username, password)
|
||||
self._client = TotalConnectClient.TotalConnectClient(username, password)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
|
|
|||
|
|
@ -11,8 +11,11 @@ import homeassistant.components.alarm_control_panel as alarm
|
|||
from homeassistant.components.verisure import CONF_ALARM, CONF_CODE_DIGITS
|
||||
from homeassistant.components.verisure import HUB as hub
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
|
||||
STATE_UNKNOWN)
|
||||
STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_DISARMED,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -29,10 +32,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||
def set_arm_state(state, code=None):
|
||||
"""Send set arm state command."""
|
||||
transaction_id = hub.session.set_arm_state(code, state)[
|
||||
'armStateChangeTransactionId']
|
||||
_LOGGER.info('verisure set arm state %s', state)
|
||||
"armStateChangeTransactionId"
|
||||
]
|
||||
_LOGGER.info("verisure set arm state %s", state)
|
||||
transaction = {}
|
||||
while 'result' not in transaction:
|
||||
while "result" not in transaction:
|
||||
sleep(0.5)
|
||||
transaction = hub.session.get_arm_state_transaction(transaction_id)
|
||||
# pylint: disable=unexpected-keyword-arg
|
||||
|
|
@ -51,7 +55,7 @@ class VerisureAlarm(alarm.AlarmControlPanel):
|
|||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return '{} alarm'.format(hub.session.installations[0]['alias'])
|
||||
return "{} alarm".format(hub.session.installations[0]["alias"])
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
|
|
@ -61,7 +65,7 @@ class VerisureAlarm(alarm.AlarmControlPanel):
|
|||
@property
|
||||
def code_format(self):
|
||||
"""Return one or more digits/characters."""
|
||||
return 'Number'
|
||||
return "Number"
|
||||
|
||||
@property
|
||||
def changed_by(self):
|
||||
|
|
@ -72,24 +76,24 @@ class VerisureAlarm(alarm.AlarmControlPanel):
|
|||
"""Update alarm status."""
|
||||
hub.update_overview()
|
||||
status = hub.get_first("$.armState.statusType")
|
||||
if status == 'DISARMED':
|
||||
if status == "DISARMED":
|
||||
self._state = STATE_ALARM_DISARMED
|
||||
elif status == 'ARMED_HOME':
|
||||
elif status == "ARMED_HOME":
|
||||
self._state = STATE_ALARM_ARMED_HOME
|
||||
elif status == 'ARMED_AWAY':
|
||||
elif status == "ARMED_AWAY":
|
||||
self._state = STATE_ALARM_ARMED_AWAY
|
||||
elif status != 'PENDING':
|
||||
_LOGGER.error('Unknown alarm state %s', status)
|
||||
elif status != "PENDING":
|
||||
_LOGGER.error("Unknown alarm state %s", status)
|
||||
self._changed_by = hub.get_first("$.armState.name")
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
set_arm_state('DISARMED', code)
|
||||
set_arm_state("DISARMED", code)
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
set_arm_state('ARMED_HOME', code)
|
||||
set_arm_state("ARMED_HOME", code)
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
set_arm_state('ARMED_AWAY', code)
|
||||
set_arm_state("ARMED_AWAY", code)
|
||||
|
|
|
|||
|
|
@ -10,14 +10,17 @@ import logging
|
|||
import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.components.wink import DOMAIN, WinkDevice
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
|
||||
STATE_UNKNOWN)
|
||||
STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_DISARMED,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['wink']
|
||||
DEPENDENCIES = ["wink"]
|
||||
|
||||
STATE_ALARM_PRIVACY = 'Private'
|
||||
STATE_ALARM_PRIVACY = "Private"
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
|
|
@ -31,7 +34,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||
camera.capability()
|
||||
except AttributeError:
|
||||
_id = camera.object_id() + camera.name()
|
||||
if _id not in hass.data[DOMAIN]['unique_ids']:
|
||||
if _id not in hass.data[DOMAIN]["unique_ids"]:
|
||||
add_entities([WinkCameraDevice(camera, hass)])
|
||||
|
||||
|
||||
|
|
@ -41,7 +44,7 @@ class WinkCameraDevice(WinkDevice, alarm.AlarmControlPanel):
|
|||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Call when entity is added to hass."""
|
||||
self.hass.data[DOMAIN]['entities']['alarm_control_panel'].append(self)
|
||||
self.hass.data[DOMAIN]["entities"]["alarm_control_panel"].append(self)
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
|
|
@ -72,6 +75,4 @@ class WinkCameraDevice(WinkDevice, alarm.AlarmControlPanel):
|
|||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return {
|
||||
'private': self.wink.private()
|
||||
}
|
||||
return {"private": self.wink.private()}
|
||||
|
|
|
|||
|
|
@ -9,28 +9,37 @@ import logging
|
|||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.alarm_control_panel import (
|
||||
AlarmControlPanel, PLATFORM_SCHEMA)
|
||||
AlarmControlPanel,
|
||||
PLATFORM_SCHEMA,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONF_PASSWORD, CONF_USERNAME, CONF_NAME,
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED)
|
||||
CONF_PASSWORD,
|
||||
CONF_USERNAME,
|
||||
CONF_NAME,
|
||||
STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_DISARMED,
|
||||
)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['yalesmartalarmclient==0.1.4']
|
||||
REQUIREMENTS = ["yalesmartalarmclient==0.1.4"]
|
||||
|
||||
CONF_AREA_ID = 'area_id'
|
||||
CONF_AREA_ID = "area_id"
|
||||
|
||||
DEFAULT_NAME = 'Yale Smart Alarm'
|
||||
DEFAULT_NAME = "Yale Smart Alarm"
|
||||
|
||||
DEFAULT_AREA_ID = '1'
|
||||
DEFAULT_AREA_ID = "1"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_AREA_ID, default=DEFAULT_AREA_ID): cv.string,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
|
|
@ -40,8 +49,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||
password = config[CONF_PASSWORD]
|
||||
area_id = config[CONF_AREA_ID]
|
||||
|
||||
from yalesmartalarmclient.client import (
|
||||
YaleSmartAlarmClient, AuthenticationError)
|
||||
from yalesmartalarmclient.client import YaleSmartAlarmClient, AuthenticationError
|
||||
|
||||
try:
|
||||
client = YaleSmartAlarmClient(username, password, area_id)
|
||||
except AuthenticationError:
|
||||
|
|
@ -60,13 +69,16 @@ class YaleAlarmDevice(AlarmControlPanel):
|
|||
self._client = client
|
||||
self._state = None
|
||||
|
||||
from yalesmartalarmclient.client import (YALE_STATE_DISARM,
|
||||
from yalesmartalarmclient.client import (
|
||||
YALE_STATE_DISARM,
|
||||
YALE_STATE_ARM_PARTIAL,
|
||||
YALE_STATE_ARM_FULL)
|
||||
YALE_STATE_ARM_FULL,
|
||||
)
|
||||
|
||||
self._state_map = {
|
||||
YALE_STATE_DISARM: STATE_ALARM_DISARMED,
|
||||
YALE_STATE_ARM_PARTIAL: STATE_ALARM_ARMED_HOME,
|
||||
YALE_STATE_ARM_FULL: STATE_ALARM_ARMED_AWAY
|
||||
YALE_STATE_ARM_FULL: STATE_ALARM_ARMED_AWAY,
|
||||
}
|
||||
|
||||
@property
|
||||
|
|
|
|||
|
|
@ -15,87 +15,108 @@ from homeassistant.helpers.discovery import load_platform
|
|||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.components.binary_sensor import DEVICE_CLASSES_SCHEMA
|
||||
|
||||
REQUIREMENTS = ['alarmdecoder==1.13.2']
|
||||
REQUIREMENTS = ["alarmdecoder==1.13.2"]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'alarmdecoder'
|
||||
DOMAIN = "alarmdecoder"
|
||||
|
||||
DATA_AD = 'alarmdecoder'
|
||||
DATA_AD = "alarmdecoder"
|
||||
|
||||
CONF_DEVICE = 'device'
|
||||
CONF_DEVICE_BAUD = 'baudrate'
|
||||
CONF_DEVICE_HOST = 'host'
|
||||
CONF_DEVICE_PATH = 'path'
|
||||
CONF_DEVICE_PORT = 'port'
|
||||
CONF_DEVICE_TYPE = 'type'
|
||||
CONF_PANEL_DISPLAY = 'panel_display'
|
||||
CONF_ZONE_NAME = 'name'
|
||||
CONF_ZONE_TYPE = 'type'
|
||||
CONF_ZONE_RFID = 'rfid'
|
||||
CONF_ZONES = 'zones'
|
||||
CONF_RELAY_ADDR = 'relayaddr'
|
||||
CONF_RELAY_CHAN = 'relaychan'
|
||||
CONF_DEVICE = "device"
|
||||
CONF_DEVICE_BAUD = "baudrate"
|
||||
CONF_DEVICE_HOST = "host"
|
||||
CONF_DEVICE_PATH = "path"
|
||||
CONF_DEVICE_PORT = "port"
|
||||
CONF_DEVICE_TYPE = "type"
|
||||
CONF_PANEL_DISPLAY = "panel_display"
|
||||
CONF_ZONE_NAME = "name"
|
||||
CONF_ZONE_TYPE = "type"
|
||||
CONF_ZONE_RFID = "rfid"
|
||||
CONF_ZONES = "zones"
|
||||
CONF_RELAY_ADDR = "relayaddr"
|
||||
CONF_RELAY_CHAN = "relaychan"
|
||||
|
||||
DEFAULT_DEVICE_TYPE = 'socket'
|
||||
DEFAULT_DEVICE_HOST = 'localhost'
|
||||
DEFAULT_DEVICE_TYPE = "socket"
|
||||
DEFAULT_DEVICE_HOST = "localhost"
|
||||
DEFAULT_DEVICE_PORT = 10000
|
||||
DEFAULT_DEVICE_PATH = '/dev/ttyUSB0'
|
||||
DEFAULT_DEVICE_PATH = "/dev/ttyUSB0"
|
||||
DEFAULT_DEVICE_BAUD = 115200
|
||||
|
||||
DEFAULT_PANEL_DISPLAY = False
|
||||
|
||||
DEFAULT_ZONE_TYPE = 'opening'
|
||||
DEFAULT_ZONE_TYPE = "opening"
|
||||
|
||||
SIGNAL_PANEL_MESSAGE = 'alarmdecoder.panel_message'
|
||||
SIGNAL_PANEL_ARM_AWAY = 'alarmdecoder.panel_arm_away'
|
||||
SIGNAL_PANEL_ARM_HOME = 'alarmdecoder.panel_arm_home'
|
||||
SIGNAL_PANEL_DISARM = 'alarmdecoder.panel_disarm'
|
||||
SIGNAL_PANEL_MESSAGE = "alarmdecoder.panel_message"
|
||||
SIGNAL_PANEL_ARM_AWAY = "alarmdecoder.panel_arm_away"
|
||||
SIGNAL_PANEL_ARM_HOME = "alarmdecoder.panel_arm_home"
|
||||
SIGNAL_PANEL_DISARM = "alarmdecoder.panel_disarm"
|
||||
|
||||
SIGNAL_ZONE_FAULT = 'alarmdecoder.zone_fault'
|
||||
SIGNAL_ZONE_RESTORE = 'alarmdecoder.zone_restore'
|
||||
SIGNAL_RFX_MESSAGE = 'alarmdecoder.rfx_message'
|
||||
SIGNAL_REL_MESSAGE = 'alarmdecoder.rel_message'
|
||||
SIGNAL_ZONE_FAULT = "alarmdecoder.zone_fault"
|
||||
SIGNAL_ZONE_RESTORE = "alarmdecoder.zone_restore"
|
||||
SIGNAL_RFX_MESSAGE = "alarmdecoder.rfx_message"
|
||||
SIGNAL_REL_MESSAGE = "alarmdecoder.rel_message"
|
||||
|
||||
DEVICE_SOCKET_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_DEVICE_TYPE): 'socket',
|
||||
DEVICE_SOCKET_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_DEVICE_TYPE): "socket",
|
||||
vol.Optional(CONF_DEVICE_HOST, default=DEFAULT_DEVICE_HOST): cv.string,
|
||||
vol.Optional(CONF_DEVICE_PORT, default=DEFAULT_DEVICE_PORT): cv.port})
|
||||
vol.Optional(CONF_DEVICE_PORT, default=DEFAULT_DEVICE_PORT): cv.port,
|
||||
}
|
||||
)
|
||||
|
||||
DEVICE_SERIAL_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_DEVICE_TYPE): 'serial',
|
||||
DEVICE_SERIAL_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_DEVICE_TYPE): "serial",
|
||||
vol.Optional(CONF_DEVICE_PATH, default=DEFAULT_DEVICE_PATH): cv.string,
|
||||
vol.Optional(CONF_DEVICE_BAUD, default=DEFAULT_DEVICE_BAUD): cv.string})
|
||||
vol.Optional(CONF_DEVICE_BAUD, default=DEFAULT_DEVICE_BAUD): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
DEVICE_USB_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_DEVICE_TYPE): 'usb'})
|
||||
DEVICE_USB_SCHEMA = vol.Schema({vol.Required(CONF_DEVICE_TYPE): "usb"})
|
||||
|
||||
ZONE_SCHEMA = vol.Schema({
|
||||
ZONE_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_ZONE_NAME): cv.string,
|
||||
vol.Optional(CONF_ZONE_TYPE,
|
||||
default=DEFAULT_ZONE_TYPE): vol.Any(DEVICE_CLASSES_SCHEMA),
|
||||
vol.Optional(CONF_ZONE_TYPE, default=DEFAULT_ZONE_TYPE): vol.Any(
|
||||
DEVICE_CLASSES_SCHEMA
|
||||
),
|
||||
vol.Optional(CONF_ZONE_RFID): cv.string,
|
||||
vol.Inclusive(CONF_RELAY_ADDR, 'relaylocation',
|
||||
'Relay address and channel must exist together'): cv.byte,
|
||||
vol.Inclusive(CONF_RELAY_CHAN, 'relaylocation',
|
||||
'Relay address and channel must exist together'): cv.byte})
|
||||
vol.Inclusive(
|
||||
CONF_RELAY_ADDR,
|
||||
"relaylocation",
|
||||
"Relay address and channel must exist together",
|
||||
): cv.byte,
|
||||
vol.Inclusive(
|
||||
CONF_RELAY_CHAN,
|
||||
"relaylocation",
|
||||
"Relay address and channel must exist together",
|
||||
): cv.byte,
|
||||
}
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
DOMAIN: vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_DEVICE): vol.Any(
|
||||
DEVICE_SOCKET_SCHEMA, DEVICE_SERIAL_SCHEMA,
|
||||
DEVICE_USB_SCHEMA),
|
||||
vol.Optional(CONF_PANEL_DISPLAY,
|
||||
default=DEFAULT_PANEL_DISPLAY): cv.boolean,
|
||||
DEVICE_SOCKET_SCHEMA, DEVICE_SERIAL_SCHEMA, DEVICE_USB_SCHEMA
|
||||
),
|
||||
vol.Optional(
|
||||
CONF_PANEL_DISPLAY, default=DEFAULT_PANEL_DISPLAY
|
||||
): cv.boolean,
|
||||
vol.Optional(CONF_ZONES): {vol.Coerce(int): ZONE_SCHEMA},
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
}
|
||||
)
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Set up for the AlarmDecoder devices."""
|
||||
from alarmdecoder import AlarmDecoder
|
||||
from alarmdecoder.devices import (SocketDevice, SerialDevice, USBDevice)
|
||||
from alarmdecoder.devices import SocketDevice, SerialDevice, USBDevice
|
||||
|
||||
conf = config.get(DOMAIN)
|
||||
|
||||
|
|
@ -120,13 +141,15 @@ def setup(hass, config):
|
|||
def open_connection(now=None):
|
||||
"""Open a connection to AlarmDecoder."""
|
||||
from alarmdecoder.util import NoDeviceError
|
||||
|
||||
nonlocal restart
|
||||
try:
|
||||
controller.open(baud)
|
||||
except NoDeviceError:
|
||||
_LOGGER.debug("Failed to connect. Retrying in 5 seconds")
|
||||
hass.helpers.event.track_point_in_time(
|
||||
open_connection, dt_util.utcnow() + timedelta(seconds=5))
|
||||
open_connection, dt_util.utcnow() + timedelta(seconds=5)
|
||||
)
|
||||
return
|
||||
_LOGGER.debug("Established a connection with the alarmdecoder")
|
||||
restart = True
|
||||
|
|
@ -142,39 +165,34 @@ def setup(hass, config):
|
|||
|
||||
def handle_message(sender, message):
|
||||
"""Handle message from AlarmDecoder."""
|
||||
hass.helpers.dispatcher.dispatcher_send(
|
||||
SIGNAL_PANEL_MESSAGE, message)
|
||||
hass.helpers.dispatcher.dispatcher_send(SIGNAL_PANEL_MESSAGE, message)
|
||||
|
||||
def handle_rfx_message(sender, message):
|
||||
"""Handle RFX message from AlarmDecoder."""
|
||||
hass.helpers.dispatcher.dispatcher_send(
|
||||
SIGNAL_RFX_MESSAGE, message)
|
||||
hass.helpers.dispatcher.dispatcher_send(SIGNAL_RFX_MESSAGE, message)
|
||||
|
||||
def zone_fault_callback(sender, zone):
|
||||
"""Handle zone fault from AlarmDecoder."""
|
||||
hass.helpers.dispatcher.dispatcher_send(
|
||||
SIGNAL_ZONE_FAULT, zone)
|
||||
hass.helpers.dispatcher.dispatcher_send(SIGNAL_ZONE_FAULT, zone)
|
||||
|
||||
def zone_restore_callback(sender, zone):
|
||||
"""Handle zone restore from AlarmDecoder."""
|
||||
hass.helpers.dispatcher.dispatcher_send(
|
||||
SIGNAL_ZONE_RESTORE, zone)
|
||||
hass.helpers.dispatcher.dispatcher_send(SIGNAL_ZONE_RESTORE, zone)
|
||||
|
||||
def handle_rel_message(sender, message):
|
||||
"""Handle relay message from AlarmDecoder."""
|
||||
hass.helpers.dispatcher.dispatcher_send(
|
||||
SIGNAL_REL_MESSAGE, message)
|
||||
hass.helpers.dispatcher.dispatcher_send(SIGNAL_REL_MESSAGE, message)
|
||||
|
||||
controller = False
|
||||
if device_type == 'socket':
|
||||
if device_type == "socket":
|
||||
host = device.get(CONF_DEVICE_HOST)
|
||||
port = device.get(CONF_DEVICE_PORT)
|
||||
controller = AlarmDecoder(SocketDevice(interface=(host, port)))
|
||||
elif device_type == 'serial':
|
||||
elif device_type == "serial":
|
||||
path = device.get(CONF_DEVICE_PATH)
|
||||
baud = device.get(CONF_DEVICE_BAUD)
|
||||
controller = AlarmDecoder(SerialDevice(interface=path))
|
||||
elif device_type == 'usb':
|
||||
elif device_type == "usb":
|
||||
AlarmDecoder(USBDevice.find())
|
||||
return False
|
||||
|
||||
|
|
@ -191,13 +209,12 @@ def setup(hass, config):
|
|||
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_alarmdecoder)
|
||||
|
||||
load_platform(hass, 'alarm_control_panel', DOMAIN, conf, config)
|
||||
load_platform(hass, "alarm_control_panel", DOMAIN, conf, config)
|
||||
|
||||
if zones:
|
||||
load_platform(
|
||||
hass, 'binary_sensor', DOMAIN, {CONF_ZONES: zones}, config)
|
||||
load_platform(hass, "binary_sensor", DOMAIN, {CONF_ZONES: zones}, config)
|
||||
|
||||
if display:
|
||||
load_platform(hass, 'sensor', DOMAIN, conf, config)
|
||||
load_platform(hass, "sensor", DOMAIN, conf, config)
|
||||
|
||||
return True
|
||||
|
|
|
|||
|
|
@ -12,27 +12,37 @@ import voluptuous as vol
|
|||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import (
|
||||
CONF_ENTITY_ID, STATE_IDLE, CONF_NAME, CONF_STATE, STATE_ON, STATE_OFF,
|
||||
SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, ATTR_ENTITY_ID)
|
||||
CONF_ENTITY_ID,
|
||||
STATE_IDLE,
|
||||
CONF_NAME,
|
||||
CONF_STATE,
|
||||
STATE_ON,
|
||||
STATE_OFF,
|
||||
SERVICE_TURN_ON,
|
||||
SERVICE_TURN_OFF,
|
||||
SERVICE_TOGGLE,
|
||||
ATTR_ENTITY_ID,
|
||||
)
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
from homeassistant.helpers import service, event
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'alert'
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
DOMAIN = "alert"
|
||||
ENTITY_ID_FORMAT = DOMAIN + ".{}"
|
||||
|
||||
CONF_DONE_MESSAGE = 'done_message'
|
||||
CONF_CAN_ACK = 'can_acknowledge'
|
||||
CONF_NOTIFIERS = 'notifiers'
|
||||
CONF_REPEAT = 'repeat'
|
||||
CONF_SKIP_FIRST = 'skip_first'
|
||||
CONF_DONE_MESSAGE = "done_message"
|
||||
CONF_CAN_ACK = "can_acknowledge"
|
||||
CONF_NOTIFIERS = "notifiers"
|
||||
CONF_REPEAT = "repeat"
|
||||
CONF_SKIP_FIRST = "skip_first"
|
||||
|
||||
DEFAULT_CAN_ACK = True
|
||||
DEFAULT_SKIP_FIRST = False
|
||||
|
||||
ALERT_SCHEMA = vol.Schema({
|
||||
ALERT_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_DONE_MESSAGE): cv.string,
|
||||
vol.Required(CONF_ENTITY_ID): cv.entity_id,
|
||||
|
|
@ -40,18 +50,16 @@ ALERT_SCHEMA = vol.Schema({
|
|||
vol.Required(CONF_REPEAT): vol.All(cv.ensure_list, [vol.Coerce(float)]),
|
||||
vol.Required(CONF_CAN_ACK, default=DEFAULT_CAN_ACK): cv.boolean,
|
||||
vol.Required(CONF_SKIP_FIRST, default=DEFAULT_SKIP_FIRST): cv.boolean,
|
||||
vol.Required(CONF_NOTIFIERS): cv.ensure_list})
|
||||
vol.Required(CONF_NOTIFIERS): cv.ensure_list,
|
||||
}
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
cv.slug: ALERT_SCHEMA,
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{DOMAIN: vol.Schema({cv.slug: ALERT_SCHEMA})}, extra=vol.ALLOW_EXTRA
|
||||
)
|
||||
|
||||
|
||||
ALERT_SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
})
|
||||
ALERT_SERVICE_SCHEMA = vol.Schema({vol.Required(ATTR_ENTITY_ID): cv.entity_ids})
|
||||
|
||||
|
||||
def is_on(hass, entity_id):
|
||||
|
|
@ -68,8 +76,7 @@ def turn_on(hass, entity_id):
|
|||
def async_turn_on(hass, entity_id):
|
||||
"""Async reset the alert."""
|
||||
data = {ATTR_ENTITY_ID: entity_id}
|
||||
hass.async_create_task(
|
||||
hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data))
|
||||
hass.async_create_task(hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data))
|
||||
|
||||
|
||||
def turn_off(hass, entity_id):
|
||||
|
|
@ -81,8 +88,7 @@ def turn_off(hass, entity_id):
|
|||
def async_turn_off(hass, entity_id):
|
||||
"""Async acknowledge the alert."""
|
||||
data = {ATTR_ENTITY_ID: entity_id}
|
||||
hass.async_create_task(
|
||||
hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, data))
|
||||
hass.async_create_task(hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, data))
|
||||
|
||||
|
||||
def toggle(hass, entity_id):
|
||||
|
|
@ -94,8 +100,7 @@ def toggle(hass, entity_id):
|
|||
def async_toggle(hass, entity_id):
|
||||
"""Async toggle acknowledgement of alert."""
|
||||
data = {ATTR_ENTITY_ID: entity_id}
|
||||
hass.async_create_task(
|
||||
hass.services.async_call(DOMAIN, SERVICE_TOGGLE, data))
|
||||
hass.async_create_task(hass.services.async_call(DOMAIN, SERVICE_TOGGLE, data))
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
|
|
@ -121,23 +126,33 @@ def async_setup(hass, config):
|
|||
|
||||
# Setup alerts
|
||||
for entity_id, alert in alerts.items():
|
||||
entity = Alert(hass, entity_id,
|
||||
alert[CONF_NAME], alert.get(CONF_DONE_MESSAGE),
|
||||
alert[CONF_ENTITY_ID], alert[CONF_STATE],
|
||||
alert[CONF_REPEAT], alert[CONF_SKIP_FIRST],
|
||||
alert[CONF_NOTIFIERS], alert[CONF_CAN_ACK])
|
||||
entity = Alert(
|
||||
hass,
|
||||
entity_id,
|
||||
alert[CONF_NAME],
|
||||
alert.get(CONF_DONE_MESSAGE),
|
||||
alert[CONF_ENTITY_ID],
|
||||
alert[CONF_STATE],
|
||||
alert[CONF_REPEAT],
|
||||
alert[CONF_SKIP_FIRST],
|
||||
alert[CONF_NOTIFIERS],
|
||||
alert[CONF_CAN_ACK],
|
||||
)
|
||||
all_alerts[entity.entity_id] = entity
|
||||
|
||||
# Setup service calls
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_TURN_OFF, async_handle_alert_service,
|
||||
schema=ALERT_SERVICE_SCHEMA)
|
||||
DOMAIN,
|
||||
SERVICE_TURN_OFF,
|
||||
async_handle_alert_service,
|
||||
schema=ALERT_SERVICE_SCHEMA,
|
||||
)
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_TURN_ON, async_handle_alert_service,
|
||||
schema=ALERT_SERVICE_SCHEMA)
|
||||
DOMAIN, SERVICE_TURN_ON, async_handle_alert_service, schema=ALERT_SERVICE_SCHEMA
|
||||
)
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_TOGGLE, async_handle_alert_service,
|
||||
schema=ALERT_SERVICE_SCHEMA)
|
||||
DOMAIN, SERVICE_TOGGLE, async_handle_alert_service, schema=ALERT_SERVICE_SCHEMA
|
||||
)
|
||||
|
||||
tasks = [alert.async_update_ha_state() for alert in all_alerts.values()]
|
||||
if tasks:
|
||||
|
|
@ -149,8 +164,19 @@ def async_setup(hass, config):
|
|||
class Alert(ToggleEntity):
|
||||
"""Representation of an alert."""
|
||||
|
||||
def __init__(self, hass, entity_id, name, done_message, watched_entity_id,
|
||||
state, repeat, skip_first, notifiers, can_ack):
|
||||
def __init__(
|
||||
self,
|
||||
hass,
|
||||
entity_id,
|
||||
name,
|
||||
done_message,
|
||||
watched_entity_id,
|
||||
state,
|
||||
repeat,
|
||||
skip_first,
|
||||
notifiers,
|
||||
can_ack,
|
||||
):
|
||||
"""Initialize the alert."""
|
||||
self.hass = hass
|
||||
self._name = name
|
||||
|
|
@ -170,7 +196,8 @@ class Alert(ToggleEntity):
|
|||
self.entity_id = ENTITY_ID_FORMAT.format(entity_id)
|
||||
|
||||
event.async_track_state_change(
|
||||
hass, watched_entity_id, self.watched_entity_change)
|
||||
hass, watched_entity_id, self.watched_entity_change
|
||||
)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
|
@ -236,8 +263,9 @@ class Alert(ToggleEntity):
|
|||
"""Schedule a notification."""
|
||||
delay = self._delay[self._next_delay]
|
||||
next_msg = datetime.now() + delay
|
||||
self._cancel = \
|
||||
event.async_track_point_in_time(self.hass, self._notify, next_msg)
|
||||
self._cancel = event.async_track_point_in_time(
|
||||
self.hass, self._notify, next_msg
|
||||
)
|
||||
self._next_delay = min(self._next_delay + 1, len(self._delay) - 1)
|
||||
|
||||
@asyncio.coroutine
|
||||
|
|
@ -251,7 +279,8 @@ class Alert(ToggleEntity):
|
|||
self._send_done_message = True
|
||||
for target in self._notifiers:
|
||||
yield from self.hass.services.async_call(
|
||||
'notify', target, {'message': self._name})
|
||||
"notify", target, {"message": self._name}
|
||||
)
|
||||
yield from self._schedule_notify()
|
||||
|
||||
@asyncio.coroutine
|
||||
|
|
@ -261,7 +290,8 @@ class Alert(ToggleEntity):
|
|||
self._send_done_message = False
|
||||
for target in self._notifiers:
|
||||
yield from self.hass.services.async_call(
|
||||
'notify', target, {'message': self._done_message})
|
||||
"notify", target, {"message": self._done_message}
|
||||
)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_turn_on(self, **kwargs):
|
||||
|
|
|
|||
|
|
@ -14,43 +14,62 @@ from homeassistant.helpers import entityfilter
|
|||
|
||||
from . import flash_briefings, intent, smart_home
|
||||
from .const import (
|
||||
CONF_AUDIO, CONF_DISPLAY_URL, CONF_TEXT, CONF_TITLE, CONF_UID, DOMAIN,
|
||||
CONF_FILTER, CONF_ENTITY_CONFIG)
|
||||
CONF_AUDIO,
|
||||
CONF_DISPLAY_URL,
|
||||
CONF_TEXT,
|
||||
CONF_TITLE,
|
||||
CONF_UID,
|
||||
DOMAIN,
|
||||
CONF_FILTER,
|
||||
CONF_ENTITY_CONFIG,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_FLASH_BRIEFINGS = 'flash_briefings'
|
||||
CONF_SMART_HOME = 'smart_home'
|
||||
CONF_FLASH_BRIEFINGS = "flash_briefings"
|
||||
CONF_SMART_HOME = "smart_home"
|
||||
|
||||
DEPENDENCIES = ['http']
|
||||
DEPENDENCIES = ["http"]
|
||||
|
||||
ALEXA_ENTITY_SCHEMA = vol.Schema({
|
||||
ALEXA_ENTITY_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(smart_home.CONF_DESCRIPTION): cv.string,
|
||||
vol.Optional(smart_home.CONF_DISPLAY_CATEGORIES): cv.string,
|
||||
vol.Optional(smart_home.CONF_NAME): cv.string,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
SMART_HOME_SCHEMA = vol.Schema({
|
||||
SMART_HOME_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_FILTER, default={}): entityfilter.FILTER_SCHEMA,
|
||||
vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: ALEXA_ENTITY_SCHEMA}
|
||||
})
|
||||
vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: ALEXA_ENTITY_SCHEMA},
|
||||
}
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
DOMAIN: {
|
||||
CONF_FLASH_BRIEFINGS: {
|
||||
cv.string: vol.All(cv.ensure_list, [{
|
||||
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)
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
|
|
|
|||
|
|
@ -1,23 +1,23 @@
|
|||
"""Constants for the Alexa integration."""
|
||||
DOMAIN = 'alexa'
|
||||
DOMAIN = "alexa"
|
||||
|
||||
# Flash briefing constants
|
||||
CONF_UID = 'uid'
|
||||
CONF_TITLE = 'title'
|
||||
CONF_AUDIO = 'audio'
|
||||
CONF_TEXT = 'text'
|
||||
CONF_DISPLAY_URL = 'display_url'
|
||||
CONF_UID = "uid"
|
||||
CONF_TITLE = "title"
|
||||
CONF_AUDIO = "audio"
|
||||
CONF_TEXT = "text"
|
||||
CONF_DISPLAY_URL = "display_url"
|
||||
|
||||
CONF_FILTER = 'filter'
|
||||
CONF_ENTITY_CONFIG = 'entity_config'
|
||||
CONF_FILTER = "filter"
|
||||
CONF_ENTITY_CONFIG = "entity_config"
|
||||
|
||||
ATTR_UID = 'uid'
|
||||
ATTR_UPDATE_DATE = 'updateDate'
|
||||
ATTR_TITLE_TEXT = 'titleText'
|
||||
ATTR_STREAM_URL = 'streamUrl'
|
||||
ATTR_MAIN_TEXT = 'mainText'
|
||||
ATTR_REDIRECTION_URL = 'redirectionURL'
|
||||
ATTR_UID = "uid"
|
||||
ATTR_UPDATE_DATE = "updateDate"
|
||||
ATTR_TITLE_TEXT = "titleText"
|
||||
ATTR_STREAM_URL = "streamUrl"
|
||||
ATTR_MAIN_TEXT = "mainText"
|
||||
ATTR_REDIRECTION_URL = "redirectionURL"
|
||||
|
||||
SYN_RESOLUTION_MATCH = 'ER_SUCCESS_MATCH'
|
||||
SYN_RESOLUTION_MATCH = "ER_SUCCESS_MATCH"
|
||||
|
||||
DATE_FORMAT = '%Y-%m-%dT%H:%M:%S.0Z'
|
||||
DATE_FORMAT = "%Y-%m-%dT%H:%M:%S.0Z"
|
||||
|
|
|
|||
|
|
@ -14,27 +14,36 @@ from homeassistant.core import callback
|
|||
from homeassistant.helpers import template
|
||||
|
||||
from .const import (
|
||||
ATTR_MAIN_TEXT, ATTR_REDIRECTION_URL, ATTR_STREAM_URL, ATTR_TITLE_TEXT,
|
||||
ATTR_UID, ATTR_UPDATE_DATE, CONF_AUDIO, CONF_DISPLAY_URL, CONF_TEXT,
|
||||
CONF_TITLE, CONF_UID, DATE_FORMAT)
|
||||
ATTR_MAIN_TEXT,
|
||||
ATTR_REDIRECTION_URL,
|
||||
ATTR_STREAM_URL,
|
||||
ATTR_TITLE_TEXT,
|
||||
ATTR_UID,
|
||||
ATTR_UPDATE_DATE,
|
||||
CONF_AUDIO,
|
||||
CONF_DISPLAY_URL,
|
||||
CONF_TEXT,
|
||||
CONF_TITLE,
|
||||
CONF_UID,
|
||||
DATE_FORMAT,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
FLASH_BRIEFINGS_API_ENDPOINT = '/api/alexa/flash_briefings/{briefing_id}'
|
||||
FLASH_BRIEFINGS_API_ENDPOINT = "/api/alexa/flash_briefings/{briefing_id}"
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup(hass, flash_briefing_config):
|
||||
"""Activate Alexa component."""
|
||||
hass.http.register_view(
|
||||
AlexaFlashBriefingView(hass, flash_briefing_config))
|
||||
hass.http.register_view(AlexaFlashBriefingView(hass, flash_briefing_config))
|
||||
|
||||
|
||||
class AlexaFlashBriefingView(http.HomeAssistantView):
|
||||
"""Handle Alexa Flash Briefing skill requests."""
|
||||
|
||||
url = FLASH_BRIEFINGS_API_ENDPOINT
|
||||
name = 'api:alexa:flash_briefings'
|
||||
name = "api:alexa:flash_briefings"
|
||||
|
||||
def __init__(self, hass, flash_briefings):
|
||||
"""Initialize Alexa view."""
|
||||
|
|
@ -45,13 +54,12 @@ class AlexaFlashBriefingView(http.HomeAssistantView):
|
|||
@callback
|
||||
def get(self, request, briefing_id):
|
||||
"""Handle Alexa Flash Briefing request."""
|
||||
_LOGGER.debug("Received Alexa flash briefing request for: %s",
|
||||
briefing_id)
|
||||
_LOGGER.debug("Received Alexa flash briefing request for: %s", briefing_id)
|
||||
|
||||
if self.flash_briefings.get(briefing_id) is None:
|
||||
err = "No configured Alexa flash briefing was found for: %s"
|
||||
_LOGGER.error(err, briefing_id)
|
||||
return b'', 404
|
||||
return b"", 404
|
||||
|
||||
briefing = []
|
||||
|
||||
|
|
@ -81,10 +89,8 @@ class AlexaFlashBriefingView(http.HomeAssistantView):
|
|||
output[ATTR_STREAM_URL] = item.get(CONF_AUDIO)
|
||||
|
||||
if item.get(CONF_DISPLAY_URL) is not None:
|
||||
if isinstance(item.get(CONF_DISPLAY_URL),
|
||||
template.Template):
|
||||
output[ATTR_REDIRECTION_URL] = \
|
||||
item[CONF_DISPLAY_URL].async_render()
|
||||
if isinstance(item.get(CONF_DISPLAY_URL), template.Template):
|
||||
output[ATTR_REDIRECTION_URL] = item[CONF_DISPLAY_URL].async_render()
|
||||
else:
|
||||
output[ATTR_REDIRECTION_URL] = item.get(CONF_DISPLAY_URL)
|
||||
|
||||
|
|
|
|||
|
|
@ -20,27 +20,24 @@ _LOGGER = logging.getLogger(__name__)
|
|||
|
||||
HANDLERS = Registry()
|
||||
|
||||
INTENTS_API_ENDPOINT = '/api/alexa'
|
||||
INTENTS_API_ENDPOINT = "/api/alexa"
|
||||
|
||||
|
||||
class SpeechType(enum.Enum):
|
||||
"""The Alexa speech types."""
|
||||
|
||||
plaintext = 'PlainText'
|
||||
ssml = 'SSML'
|
||||
plaintext = "PlainText"
|
||||
ssml = "SSML"
|
||||
|
||||
|
||||
SPEECH_MAPPINGS = {
|
||||
'plain': SpeechType.plaintext,
|
||||
'ssml': SpeechType.ssml,
|
||||
}
|
||||
SPEECH_MAPPINGS = {"plain": SpeechType.plaintext, "ssml": SpeechType.ssml}
|
||||
|
||||
|
||||
class CardType(enum.Enum):
|
||||
"""The Alexa card types."""
|
||||
|
||||
simple = 'Simple'
|
||||
link_account = 'LinkAccount'
|
||||
simple = "Simple"
|
||||
link_account = "LinkAccount"
|
||||
|
||||
|
||||
@callback
|
||||
|
|
@ -57,45 +54,51 @@ class AlexaIntentsView(http.HomeAssistantView):
|
|||
"""Handle Alexa requests."""
|
||||
|
||||
url = INTENTS_API_ENDPOINT
|
||||
name = 'api:alexa'
|
||||
name = "api:alexa"
|
||||
|
||||
@asyncio.coroutine
|
||||
def post(self, request):
|
||||
"""Handle Alexa."""
|
||||
hass = request.app['hass']
|
||||
hass = request.app["hass"]
|
||||
message = yield from request.json()
|
||||
|
||||
_LOGGER.debug("Received Alexa request: %s", message)
|
||||
|
||||
try:
|
||||
response = yield from async_handle_message(hass, message)
|
||||
return b'' if response is None else self.json(response)
|
||||
return b"" if response is None else self.json(response)
|
||||
except UnknownRequest as err:
|
||||
_LOGGER.warning(str(err))
|
||||
return self.json(intent_error_response(
|
||||
hass, message, str(err)))
|
||||
return self.json(intent_error_response(hass, message, str(err)))
|
||||
|
||||
except intent.UnknownIntent as err:
|
||||
_LOGGER.warning(str(err))
|
||||
return self.json(intent_error_response(
|
||||
hass, message,
|
||||
"This intent is not yet configured within Home Assistant."))
|
||||
return self.json(
|
||||
intent_error_response(
|
||||
hass,
|
||||
message,
|
||||
"This intent is not yet configured within Home Assistant.",
|
||||
)
|
||||
)
|
||||
|
||||
except intent.InvalidSlotInfo as err:
|
||||
_LOGGER.error("Received invalid slot data from Alexa: %s", err)
|
||||
return self.json(intent_error_response(
|
||||
hass, message,
|
||||
"Invalid slot information received for this intent."))
|
||||
return self.json(
|
||||
intent_error_response(
|
||||
hass, message, "Invalid slot information received for this intent."
|
||||
)
|
||||
)
|
||||
|
||||
except intent.IntentError as err:
|
||||
_LOGGER.exception(str(err))
|
||||
return self.json(intent_error_response(
|
||||
hass, message, "Error handling intent."))
|
||||
return self.json(
|
||||
intent_error_response(hass, message, "Error handling intent.")
|
||||
)
|
||||
|
||||
|
||||
def intent_error_response(hass, message, error):
|
||||
"""Return an Alexa response that will speak the error message."""
|
||||
alexa_intent_info = message.get('request').get('intent')
|
||||
alexa_intent_info = message.get("request").get("intent")
|
||||
alexa_response = AlexaResponse(hass, alexa_intent_info)
|
||||
alexa_response.add_speech(SpeechType.plaintext, error)
|
||||
return alexa_response.as_dict()
|
||||
|
|
@ -112,26 +115,26 @@ def async_handle_message(hass, message):
|
|||
- intent.IntentError
|
||||
|
||||
"""
|
||||
req = message.get('request')
|
||||
req_type = req['type']
|
||||
req = message.get("request")
|
||||
req_type = req["type"]
|
||||
|
||||
handler = HANDLERS.get(req_type)
|
||||
|
||||
if not handler:
|
||||
raise UnknownRequest('Received unknown request {}'.format(req_type))
|
||||
raise UnknownRequest("Received unknown request {}".format(req_type))
|
||||
|
||||
return (yield from handler(hass, message))
|
||||
|
||||
|
||||
@HANDLERS.register('SessionEndedRequest')
|
||||
@HANDLERS.register("SessionEndedRequest")
|
||||
@asyncio.coroutine
|
||||
def async_handle_session_end(hass, message):
|
||||
"""Handle a session end request."""
|
||||
return None
|
||||
|
||||
|
||||
@HANDLERS.register('IntentRequest')
|
||||
@HANDLERS.register('LaunchRequest')
|
||||
@HANDLERS.register("IntentRequest")
|
||||
@HANDLERS.register("LaunchRequest")
|
||||
@asyncio.coroutine
|
||||
def async_handle_intent(hass, message):
|
||||
"""Handle an intent request.
|
||||
|
|
@ -142,33 +145,37 @@ def async_handle_intent(hass, message):
|
|||
- intent.IntentError
|
||||
|
||||
"""
|
||||
req = message.get('request')
|
||||
alexa_intent_info = req.get('intent')
|
||||
req = message.get("request")
|
||||
alexa_intent_info = req.get("intent")
|
||||
alexa_response = AlexaResponse(hass, alexa_intent_info)
|
||||
|
||||
if req['type'] == 'LaunchRequest':
|
||||
intent_name = message.get('session', {}) \
|
||||
.get('application', {}) \
|
||||
.get('applicationId')
|
||||
if req["type"] == "LaunchRequest":
|
||||
intent_name = (
|
||||
message.get("session", {}).get("application", {}).get("applicationId")
|
||||
)
|
||||
else:
|
||||
intent_name = alexa_intent_info['name']
|
||||
intent_name = alexa_intent_info["name"]
|
||||
|
||||
intent_response = yield from intent.async_handle(
|
||||
hass, DOMAIN, intent_name,
|
||||
{key: {'value': value} for key, value
|
||||
in alexa_response.variables.items()})
|
||||
hass,
|
||||
DOMAIN,
|
||||
intent_name,
|
||||
{key: {"value": value} for key, value in alexa_response.variables.items()},
|
||||
)
|
||||
|
||||
for intent_speech, alexa_speech in SPEECH_MAPPINGS.items():
|
||||
if intent_speech in intent_response.speech:
|
||||
alexa_response.add_speech(
|
||||
alexa_speech,
|
||||
intent_response.speech[intent_speech]['speech'])
|
||||
alexa_speech, intent_response.speech[intent_speech]["speech"]
|
||||
)
|
||||
break
|
||||
|
||||
if 'simple' in intent_response.card:
|
||||
if "simple" in intent_response.card:
|
||||
alexa_response.add_card(
|
||||
CardType.simple, intent_response.card['simple']['title'],
|
||||
intent_response.card['simple']['content'])
|
||||
CardType.simple,
|
||||
intent_response.card["simple"]["title"],
|
||||
intent_response.card["simple"]["content"],
|
||||
)
|
||||
|
||||
return alexa_response.as_dict()
|
||||
|
||||
|
|
@ -178,23 +185,23 @@ def resolve_slot_synonyms(key, request):
|
|||
# Default to the spoken slot value if more than one or none are found. For
|
||||
# reference to the request object structure, see the Alexa docs:
|
||||
# https://tinyurl.com/ybvm7jhs
|
||||
resolved_value = request['value']
|
||||
resolved_value = request["value"]
|
||||
|
||||
if ('resolutions' in request and
|
||||
'resolutionsPerAuthority' in request['resolutions'] and
|
||||
len(request['resolutions']['resolutionsPerAuthority']) >= 1):
|
||||
if (
|
||||
"resolutions" in request
|
||||
and "resolutionsPerAuthority" in request["resolutions"]
|
||||
and len(request["resolutions"]["resolutionsPerAuthority"]) >= 1
|
||||
):
|
||||
|
||||
# Extract all of the possible values from each authority with a
|
||||
# successful match
|
||||
possible_values = []
|
||||
|
||||
for entry in request['resolutions']['resolutionsPerAuthority']:
|
||||
if entry['status']['code'] != SYN_RESOLUTION_MATCH:
|
||||
for entry in request["resolutions"]["resolutionsPerAuthority"]:
|
||||
if entry["status"]["code"] != SYN_RESOLUTION_MATCH:
|
||||
continue
|
||||
|
||||
possible_values.extend([item['value']['name']
|
||||
for item
|
||||
in entry['values']])
|
||||
possible_values.extend([item["value"]["name"] for item in entry["values"]])
|
||||
|
||||
# If there is only one match use the resolved value, otherwise the
|
||||
# resolution cannot be determined, so use the spoken slot value
|
||||
|
|
@ -202,9 +209,9 @@ def resolve_slot_synonyms(key, request):
|
|||
resolved_value = possible_values[0]
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
'Found multiple synonym resolutions for slot value: {%s: %s}',
|
||||
"Found multiple synonym resolutions for slot value: {%s: %s}",
|
||||
key,
|
||||
request['value']
|
||||
request["value"],
|
||||
)
|
||||
|
||||
return resolved_value
|
||||
|
|
@ -225,12 +232,12 @@ class AlexaResponse:
|
|||
|
||||
# Intent is None if request was a LaunchRequest or SessionEndedRequest
|
||||
if intent_info is not None:
|
||||
for key, value in intent_info.get('slots', {}).items():
|
||||
for key, value in intent_info.get("slots", {}).items():
|
||||
# Only include slots with values
|
||||
if 'value' not in value:
|
||||
if "value" not in value:
|
||||
continue
|
||||
|
||||
_key = key.replace('.', '_')
|
||||
_key = key.replace(".", "_")
|
||||
|
||||
self.variables[_key] = resolve_slot_synonyms(key, value)
|
||||
|
||||
|
|
@ -238,9 +245,7 @@ class AlexaResponse:
|
|||
"""Add a card to the response."""
|
||||
assert self.card is None
|
||||
|
||||
card = {
|
||||
"type": card_type.value
|
||||
}
|
||||
card = {"type": card_type.value}
|
||||
|
||||
if card_type == CardType.link_account:
|
||||
self.card = card
|
||||
|
|
@ -254,43 +259,36 @@ class AlexaResponse:
|
|||
"""Add speech to the response."""
|
||||
assert self.speech is None
|
||||
|
||||
key = 'ssml' if speech_type == SpeechType.ssml else 'text'
|
||||
key = "ssml" if speech_type == SpeechType.ssml else "text"
|
||||
|
||||
self.speech = {
|
||||
'type': speech_type.value,
|
||||
key: text
|
||||
}
|
||||
self.speech = {"type": speech_type.value, key: text}
|
||||
|
||||
def add_reprompt(self, speech_type, text):
|
||||
"""Add reprompt if user does not answer."""
|
||||
assert self.reprompt is None
|
||||
|
||||
key = 'ssml' if speech_type == SpeechType.ssml else 'text'
|
||||
key = "ssml" if speech_type == SpeechType.ssml else "text"
|
||||
|
||||
self.reprompt = {
|
||||
'type': speech_type.value,
|
||||
key: text.async_render(self.variables)
|
||||
"type": speech_type.value,
|
||||
key: text.async_render(self.variables),
|
||||
}
|
||||
|
||||
def as_dict(self):
|
||||
"""Return response in an Alexa valid dict."""
|
||||
response = {
|
||||
'shouldEndSession': self.should_end_session
|
||||
}
|
||||
response = {"shouldEndSession": self.should_end_session}
|
||||
|
||||
if self.card is not None:
|
||||
response['card'] = self.card
|
||||
response["card"] = self.card
|
||||
|
||||
if self.speech is not None:
|
||||
response['outputSpeech'] = self.speech
|
||||
response["outputSpeech"] = self.speech
|
||||
|
||||
if self.reprompt is not None:
|
||||
response['reprompt'] = {
|
||||
'outputSpeech': self.reprompt
|
||||
}
|
||||
response["reprompt"] = {"outputSpeech": self.reprompt}
|
||||
|
||||
return {
|
||||
'version': '1.0',
|
||||
'sessionAttributes': self.session_attributes,
|
||||
'response': response,
|
||||
"version": "1.0",
|
||||
"sessionAttributes": self.session_attributes,
|
||||
"response": response,
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -13,85 +13,100 @@ from requests.exceptions import HTTPError, ConnectTimeout
|
|||
from requests.exceptions import ConnectionError as ConnectError
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_HOST, CONF_PORT, CONF_USERNAME, CONF_PASSWORD,
|
||||
CONF_SENSORS, CONF_SWITCHES, CONF_SCAN_INTERVAL, HTTP_BASIC_AUTHENTICATION)
|
||||
CONF_NAME,
|
||||
CONF_HOST,
|
||||
CONF_PORT,
|
||||
CONF_USERNAME,
|
||||
CONF_PASSWORD,
|
||||
CONF_SENSORS,
|
||||
CONF_SWITCHES,
|
||||
CONF_SCAN_INTERVAL,
|
||||
HTTP_BASIC_AUTHENTICATION,
|
||||
)
|
||||
from homeassistant.helpers import discovery
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['amcrest==1.2.3']
|
||||
DEPENDENCIES = ['ffmpeg']
|
||||
REQUIREMENTS = ["amcrest==1.2.3"]
|
||||
DEPENDENCIES = ["ffmpeg"]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_AUTHENTICATION = 'authentication'
|
||||
CONF_RESOLUTION = 'resolution'
|
||||
CONF_STREAM_SOURCE = 'stream_source'
|
||||
CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments'
|
||||
CONF_AUTHENTICATION = "authentication"
|
||||
CONF_RESOLUTION = "resolution"
|
||||
CONF_STREAM_SOURCE = "stream_source"
|
||||
CONF_FFMPEG_ARGUMENTS = "ffmpeg_arguments"
|
||||
|
||||
DEFAULT_NAME = 'Amcrest Camera'
|
||||
DEFAULT_NAME = "Amcrest Camera"
|
||||
DEFAULT_PORT = 80
|
||||
DEFAULT_RESOLUTION = 'high'
|
||||
DEFAULT_STREAM_SOURCE = 'snapshot'
|
||||
DEFAULT_RESOLUTION = "high"
|
||||
DEFAULT_STREAM_SOURCE = "snapshot"
|
||||
TIMEOUT = 10
|
||||
|
||||
DATA_AMCREST = 'amcrest'
|
||||
DOMAIN = 'amcrest'
|
||||
DATA_AMCREST = "amcrest"
|
||||
DOMAIN = "amcrest"
|
||||
|
||||
NOTIFICATION_ID = 'amcrest_notification'
|
||||
NOTIFICATION_TITLE = 'Amcrest Camera Setup'
|
||||
NOTIFICATION_ID = "amcrest_notification"
|
||||
NOTIFICATION_TITLE = "Amcrest Camera Setup"
|
||||
|
||||
RESOLUTION_LIST = {
|
||||
'high': 0,
|
||||
'low': 1,
|
||||
}
|
||||
RESOLUTION_LIST = {"high": 0, "low": 1}
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=10)
|
||||
|
||||
AUTHENTICATION_LIST = {
|
||||
'basic': 'basic'
|
||||
}
|
||||
AUTHENTICATION_LIST = {"basic": "basic"}
|
||||
|
||||
STREAM_SOURCE_LIST = {
|
||||
'mjpeg': 0,
|
||||
'snapshot': 1,
|
||||
'rtsp': 2,
|
||||
}
|
||||
STREAM_SOURCE_LIST = {"mjpeg": 0, "snapshot": 1, "rtsp": 2}
|
||||
|
||||
# Sensor types are defined like: Name, units, icon
|
||||
SENSORS = {
|
||||
'motion_detector': ['Motion Detected', None, 'mdi:run'],
|
||||
'sdcard': ['SD Used', '%', 'mdi:sd'],
|
||||
'ptz_preset': ['PTZ Preset', None, 'mdi:camera-iris'],
|
||||
"motion_detector": ["Motion Detected", None, "mdi:run"],
|
||||
"sdcard": ["SD Used", "%", "mdi:sd"],
|
||||
"ptz_preset": ["PTZ Preset", None, "mdi:camera-iris"],
|
||||
}
|
||||
|
||||
# Switch types are defined like: Name, icon
|
||||
SWITCHES = {
|
||||
'motion_detection': ['Motion Detection', 'mdi:run-fast'],
|
||||
'motion_recording': ['Motion Recording', 'mdi:record-rec']
|
||||
"motion_detection": ["Motion Detection", "mdi:run-fast"],
|
||||
"motion_recording": ["Motion Recording", "mdi:record-rec"],
|
||||
}
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.All(cv.ensure_list, [vol.Schema({
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
DOMAIN: vol.All(
|
||||
cv.ensure_list,
|
||||
[
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_AUTHENTICATION, default=HTTP_BASIC_AUTHENTICATION):
|
||||
vol.All(vol.In(AUTHENTICATION_LIST)),
|
||||
vol.Optional(CONF_RESOLUTION, default=DEFAULT_RESOLUTION):
|
||||
vol.All(vol.In(RESOLUTION_LIST)),
|
||||
vol.Optional(CONF_STREAM_SOURCE, default=DEFAULT_STREAM_SOURCE):
|
||||
vol.All(vol.In(STREAM_SOURCE_LIST)),
|
||||
vol.Optional(
|
||||
CONF_AUTHENTICATION, default=HTTP_BASIC_AUTHENTICATION
|
||||
): vol.All(vol.In(AUTHENTICATION_LIST)),
|
||||
vol.Optional(
|
||||
CONF_RESOLUTION, default=DEFAULT_RESOLUTION
|
||||
): vol.All(vol.In(RESOLUTION_LIST)),
|
||||
vol.Optional(
|
||||
CONF_STREAM_SOURCE, default=DEFAULT_STREAM_SOURCE
|
||||
): vol.All(vol.In(STREAM_SOURCE_LIST)),
|
||||
vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string,
|
||||
vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL):
|
||||
cv.time_period,
|
||||
vol.Optional(CONF_SENSORS):
|
||||
vol.All(cv.ensure_list, [vol.In(SENSORS)]),
|
||||
vol.Optional(CONF_SWITCHES):
|
||||
vol.All(cv.ensure_list, [vol.In(SWITCHES)]),
|
||||
})])
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
vol.Optional(
|
||||
CONF_SCAN_INTERVAL, default=SCAN_INTERVAL
|
||||
): cv.time_period,
|
||||
vol.Optional(CONF_SENSORS): vol.All(
|
||||
cv.ensure_list, [vol.In(SENSORS)]
|
||||
),
|
||||
vol.Optional(CONF_SWITCHES): vol.All(
|
||||
cv.ensure_list, [vol.In(SWITCHES)]
|
||||
),
|
||||
}
|
||||
)
|
||||
],
|
||||
)
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
|
|
@ -103,21 +118,24 @@ def setup(hass, config):
|
|||
|
||||
for device in amcrest_cams:
|
||||
try:
|
||||
camera = AmcrestCamera(device.get(CONF_HOST),
|
||||
camera = AmcrestCamera(
|
||||
device.get(CONF_HOST),
|
||||
device.get(CONF_PORT),
|
||||
device.get(CONF_USERNAME),
|
||||
device.get(CONF_PASSWORD)).camera
|
||||
device.get(CONF_PASSWORD),
|
||||
).camera
|
||||
# pylint: disable=pointless-statement
|
||||
camera.current_time
|
||||
|
||||
except (ConnectError, ConnectTimeout, HTTPError) as ex:
|
||||
_LOGGER.error("Unable to connect to Amcrest camera: %s", str(ex))
|
||||
hass.components.persistent_notification.create(
|
||||
'Error: {}<br />'
|
||||
'You will need to restart hass after fixing.'
|
||||
''.format(ex),
|
||||
"Error: {}<br />"
|
||||
"You will need to restart hass after fixing."
|
||||
"".format(ex),
|
||||
title=NOTIFICATION_TITLE,
|
||||
notification_id=NOTIFICATION_ID)
|
||||
notification_id=NOTIFICATION_ID,
|
||||
)
|
||||
continue
|
||||
|
||||
ffmpeg_arguments = device.get(CONF_FFMPEG_ARGUMENTS)
|
||||
|
|
@ -139,27 +157,24 @@ def setup(hass, config):
|
|||
authentication = None
|
||||
|
||||
hass.data[DATA_AMCREST][name] = AmcrestDevice(
|
||||
camera, name, authentication, ffmpeg_arguments, stream_source,
|
||||
resolution)
|
||||
camera, name, authentication, ffmpeg_arguments, stream_source, resolution
|
||||
)
|
||||
|
||||
discovery.load_platform(
|
||||
hass, 'camera', DOMAIN, {
|
||||
CONF_NAME: name,
|
||||
}, config)
|
||||
discovery.load_platform(hass, "camera", DOMAIN, {CONF_NAME: name}, config)
|
||||
|
||||
if sensors:
|
||||
discovery.load_platform(
|
||||
hass, 'sensor', DOMAIN, {
|
||||
CONF_NAME: name,
|
||||
CONF_SENSORS: sensors,
|
||||
}, config)
|
||||
hass, "sensor", DOMAIN, {CONF_NAME: name, CONF_SENSORS: sensors}, config
|
||||
)
|
||||
|
||||
if switches:
|
||||
discovery.load_platform(
|
||||
hass, 'switch', DOMAIN, {
|
||||
CONF_NAME: name,
|
||||
CONF_SWITCHES: switches
|
||||
}, config)
|
||||
hass,
|
||||
"switch",
|
||||
DOMAIN,
|
||||
{CONF_NAME: name, CONF_SWITCHES: switches},
|
||||
config,
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
|
@ -167,8 +182,9 @@ def setup(hass, config):
|
|||
class AmcrestDevice:
|
||||
"""Representation of a base Amcrest discovery device."""
|
||||
|
||||
def __init__(self, camera, name, authentication, ffmpeg_arguments,
|
||||
stream_source, resolution):
|
||||
def __init__(
|
||||
self, camera, name, authentication, ffmpeg_arguments, stream_source, resolution
|
||||
):
|
||||
"""Initialize the entity."""
|
||||
self.device = camera
|
||||
self.name = name
|
||||
|
|
|
|||
|
|
@ -12,141 +12,183 @@ import voluptuous as vol
|
|||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_HOST, CONF_PORT, CONF_USERNAME, CONF_PASSWORD,
|
||||
CONF_SENSORS, CONF_SWITCHES, CONF_TIMEOUT, CONF_SCAN_INTERVAL,
|
||||
CONF_PLATFORM)
|
||||
CONF_NAME,
|
||||
CONF_HOST,
|
||||
CONF_PORT,
|
||||
CONF_USERNAME,
|
||||
CONF_PASSWORD,
|
||||
CONF_SENSORS,
|
||||
CONF_SWITCHES,
|
||||
CONF_TIMEOUT,
|
||||
CONF_SCAN_INTERVAL,
|
||||
CONF_PLATFORM,
|
||||
)
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers import discovery
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_send, async_dispatcher_connect)
|
||||
async_dispatcher_send,
|
||||
async_dispatcher_connect,
|
||||
)
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.event import async_track_point_in_utc_time
|
||||
from homeassistant.util.dt import utcnow
|
||||
from homeassistant.components.camera.mjpeg import (
|
||||
CONF_MJPEG_URL, CONF_STILL_IMAGE_URL)
|
||||
from homeassistant.components.camera.mjpeg import CONF_MJPEG_URL, CONF_STILL_IMAGE_URL
|
||||
|
||||
REQUIREMENTS = ['pydroid-ipcam==0.8']
|
||||
REQUIREMENTS = ["pydroid-ipcam==0.8"]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_AUD_CONNS = 'Audio Connections'
|
||||
ATTR_HOST = 'host'
|
||||
ATTR_VID_CONNS = 'Video Connections'
|
||||
ATTR_AUD_CONNS = "Audio Connections"
|
||||
ATTR_HOST = "host"
|
||||
ATTR_VID_CONNS = "Video Connections"
|
||||
|
||||
CONF_MOTION_SENSOR = 'motion_sensor'
|
||||
CONF_MOTION_SENSOR = "motion_sensor"
|
||||
|
||||
DATA_IP_WEBCAM = 'android_ip_webcam'
|
||||
DEFAULT_NAME = 'IP Webcam'
|
||||
DATA_IP_WEBCAM = "android_ip_webcam"
|
||||
DEFAULT_NAME = "IP Webcam"
|
||||
DEFAULT_PORT = 8080
|
||||
DEFAULT_TIMEOUT = 10
|
||||
DOMAIN = 'android_ip_webcam'
|
||||
DOMAIN = "android_ip_webcam"
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=10)
|
||||
SIGNAL_UPDATE_DATA = 'android_ip_webcam_update'
|
||||
SIGNAL_UPDATE_DATA = "android_ip_webcam_update"
|
||||
|
||||
KEY_MAP = {
|
||||
'audio_connections': 'Audio Connections',
|
||||
'adet_limit': 'Audio Trigger Limit',
|
||||
'antibanding': 'Anti-banding',
|
||||
'audio_only': 'Audio Only',
|
||||
'battery_level': 'Battery Level',
|
||||
'battery_temp': 'Battery Temperature',
|
||||
'battery_voltage': 'Battery Voltage',
|
||||
'coloreffect': 'Color Effect',
|
||||
'exposure': 'Exposure Level',
|
||||
'exposure_lock': 'Exposure Lock',
|
||||
'ffc': 'Front-facing Camera',
|
||||
'flashmode': 'Flash Mode',
|
||||
'focus': 'Focus',
|
||||
'focus_homing': 'Focus Homing',
|
||||
'focus_region': 'Focus Region',
|
||||
'focusmode': 'Focus Mode',
|
||||
'gps_active': 'GPS Active',
|
||||
'idle': 'Idle',
|
||||
'ip_address': 'IPv4 Address',
|
||||
'ipv6_address': 'IPv6 Address',
|
||||
'ivideon_streaming': 'Ivideon Streaming',
|
||||
'light': 'Light Level',
|
||||
'mirror_flip': 'Mirror Flip',
|
||||
'motion': 'Motion',
|
||||
'motion_active': 'Motion Active',
|
||||
'motion_detect': 'Motion Detection',
|
||||
'motion_event': 'Motion Event',
|
||||
'motion_limit': 'Motion Limit',
|
||||
'night_vision': 'Night Vision',
|
||||
'night_vision_average': 'Night Vision Average',
|
||||
'night_vision_gain': 'Night Vision Gain',
|
||||
'orientation': 'Orientation',
|
||||
'overlay': 'Overlay',
|
||||
'photo_size': 'Photo Size',
|
||||
'pressure': 'Pressure',
|
||||
'proximity': 'Proximity',
|
||||
'quality': 'Quality',
|
||||
'scenemode': 'Scene Mode',
|
||||
'sound': 'Sound',
|
||||
'sound_event': 'Sound Event',
|
||||
'sound_timeout': 'Sound Timeout',
|
||||
'torch': 'Torch',
|
||||
'video_connections': 'Video Connections',
|
||||
'video_chunk_len': 'Video Chunk Length',
|
||||
'video_recording': 'Video Recording',
|
||||
'video_size': 'Video Size',
|
||||
'whitebalance': 'White Balance',
|
||||
'whitebalance_lock': 'White Balance Lock',
|
||||
'zoom': 'Zoom'
|
||||
"audio_connections": "Audio Connections",
|
||||
"adet_limit": "Audio Trigger Limit",
|
||||
"antibanding": "Anti-banding",
|
||||
"audio_only": "Audio Only",
|
||||
"battery_level": "Battery Level",
|
||||
"battery_temp": "Battery Temperature",
|
||||
"battery_voltage": "Battery Voltage",
|
||||
"coloreffect": "Color Effect",
|
||||
"exposure": "Exposure Level",
|
||||
"exposure_lock": "Exposure Lock",
|
||||
"ffc": "Front-facing Camera",
|
||||
"flashmode": "Flash Mode",
|
||||
"focus": "Focus",
|
||||
"focus_homing": "Focus Homing",
|
||||
"focus_region": "Focus Region",
|
||||
"focusmode": "Focus Mode",
|
||||
"gps_active": "GPS Active",
|
||||
"idle": "Idle",
|
||||
"ip_address": "IPv4 Address",
|
||||
"ipv6_address": "IPv6 Address",
|
||||
"ivideon_streaming": "Ivideon Streaming",
|
||||
"light": "Light Level",
|
||||
"mirror_flip": "Mirror Flip",
|
||||
"motion": "Motion",
|
||||
"motion_active": "Motion Active",
|
||||
"motion_detect": "Motion Detection",
|
||||
"motion_event": "Motion Event",
|
||||
"motion_limit": "Motion Limit",
|
||||
"night_vision": "Night Vision",
|
||||
"night_vision_average": "Night Vision Average",
|
||||
"night_vision_gain": "Night Vision Gain",
|
||||
"orientation": "Orientation",
|
||||
"overlay": "Overlay",
|
||||
"photo_size": "Photo Size",
|
||||
"pressure": "Pressure",
|
||||
"proximity": "Proximity",
|
||||
"quality": "Quality",
|
||||
"scenemode": "Scene Mode",
|
||||
"sound": "Sound",
|
||||
"sound_event": "Sound Event",
|
||||
"sound_timeout": "Sound Timeout",
|
||||
"torch": "Torch",
|
||||
"video_connections": "Video Connections",
|
||||
"video_chunk_len": "Video Chunk Length",
|
||||
"video_recording": "Video Recording",
|
||||
"video_size": "Video Size",
|
||||
"whitebalance": "White Balance",
|
||||
"whitebalance_lock": "White Balance Lock",
|
||||
"zoom": "Zoom",
|
||||
}
|
||||
|
||||
ICON_MAP = {
|
||||
'audio_connections': 'mdi:speaker',
|
||||
'battery_level': 'mdi:battery',
|
||||
'battery_temp': 'mdi:thermometer',
|
||||
'battery_voltage': 'mdi:battery-charging-100',
|
||||
'exposure_lock': 'mdi:camera',
|
||||
'ffc': 'mdi:camera-front-variant',
|
||||
'focus': 'mdi:image-filter-center-focus',
|
||||
'gps_active': 'mdi:crosshairs-gps',
|
||||
'light': 'mdi:flashlight',
|
||||
'motion': 'mdi:run',
|
||||
'night_vision': 'mdi:weather-night',
|
||||
'overlay': 'mdi:monitor',
|
||||
'pressure': 'mdi:gauge',
|
||||
'proximity': 'mdi:map-marker-radius',
|
||||
'quality': 'mdi:quality-high',
|
||||
'sound': 'mdi:speaker',
|
||||
'sound_event': 'mdi:speaker',
|
||||
'sound_timeout': 'mdi:speaker',
|
||||
'torch': 'mdi:white-balance-sunny',
|
||||
'video_chunk_len': 'mdi:video',
|
||||
'video_connections': 'mdi:eye',
|
||||
'video_recording': 'mdi:record-rec',
|
||||
'whitebalance_lock': 'mdi:white-balance-auto'
|
||||
"audio_connections": "mdi:speaker",
|
||||
"battery_level": "mdi:battery",
|
||||
"battery_temp": "mdi:thermometer",
|
||||
"battery_voltage": "mdi:battery-charging-100",
|
||||
"exposure_lock": "mdi:camera",
|
||||
"ffc": "mdi:camera-front-variant",
|
||||
"focus": "mdi:image-filter-center-focus",
|
||||
"gps_active": "mdi:crosshairs-gps",
|
||||
"light": "mdi:flashlight",
|
||||
"motion": "mdi:run",
|
||||
"night_vision": "mdi:weather-night",
|
||||
"overlay": "mdi:monitor",
|
||||
"pressure": "mdi:gauge",
|
||||
"proximity": "mdi:map-marker-radius",
|
||||
"quality": "mdi:quality-high",
|
||||
"sound": "mdi:speaker",
|
||||
"sound_event": "mdi:speaker",
|
||||
"sound_timeout": "mdi:speaker",
|
||||
"torch": "mdi:white-balance-sunny",
|
||||
"video_chunk_len": "mdi:video",
|
||||
"video_connections": "mdi:eye",
|
||||
"video_recording": "mdi:record-rec",
|
||||
"whitebalance_lock": "mdi:white-balance-auto",
|
||||
}
|
||||
|
||||
SWITCHES = ['exposure_lock', 'ffc', 'focus', 'gps_active', 'night_vision',
|
||||
'overlay', 'torch', 'whitebalance_lock', 'video_recording']
|
||||
SWITCHES = [
|
||||
"exposure_lock",
|
||||
"ffc",
|
||||
"focus",
|
||||
"gps_active",
|
||||
"night_vision",
|
||||
"overlay",
|
||||
"torch",
|
||||
"whitebalance_lock",
|
||||
"video_recording",
|
||||
]
|
||||
|
||||
SENSORS = ['audio_connections', 'battery_level', 'battery_temp',
|
||||
'battery_voltage', 'light', 'motion', 'pressure', 'proximity',
|
||||
'sound', 'video_connections']
|
||||
SENSORS = [
|
||||
"audio_connections",
|
||||
"battery_level",
|
||||
"battery_temp",
|
||||
"battery_voltage",
|
||||
"light",
|
||||
"motion",
|
||||
"pressure",
|
||||
"proximity",
|
||||
"sound",
|
||||
"video_connections",
|
||||
]
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.All(cv.ensure_list, [vol.Schema({
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
DOMAIN: vol.All(
|
||||
cv.ensure_list,
|
||||
[
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
|
||||
vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL):
|
||||
cv.time_period,
|
||||
vol.Inclusive(CONF_USERNAME, 'authentication'): cv.string,
|
||||
vol.Inclusive(CONF_PASSWORD, 'authentication'): cv.string,
|
||||
vol.Optional(CONF_SWITCHES):
|
||||
vol.All(cv.ensure_list, [vol.In(SWITCHES)]),
|
||||
vol.Optional(CONF_SENSORS):
|
||||
vol.All(cv.ensure_list, [vol.In(SENSORS)]),
|
||||
vol.Optional(
|
||||
CONF_TIMEOUT, default=DEFAULT_TIMEOUT
|
||||
): cv.positive_int,
|
||||
vol.Optional(
|
||||
CONF_SCAN_INTERVAL, default=SCAN_INTERVAL
|
||||
): cv.time_period,
|
||||
vol.Inclusive(CONF_USERNAME, "authentication"): cv.string,
|
||||
vol.Inclusive(CONF_PASSWORD, "authentication"): cv.string,
|
||||
vol.Optional(CONF_SWITCHES): vol.All(
|
||||
cv.ensure_list, [vol.In(SWITCHES)]
|
||||
),
|
||||
vol.Optional(CONF_SENSORS): vol.All(
|
||||
cv.ensure_list, [vol.In(SENSORS)]
|
||||
),
|
||||
vol.Optional(CONF_MOTION_SENSOR): cv.boolean,
|
||||
})])
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
}
|
||||
)
|
||||
],
|
||||
)
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
|
|
@ -171,22 +213,26 @@ def async_setup(hass, config):
|
|||
|
||||
# Init ip webcam
|
||||
cam = PyDroidIPCam(
|
||||
hass.loop, websession, host, cam_config[CONF_PORT],
|
||||
username=username, password=password,
|
||||
timeout=cam_config[CONF_TIMEOUT]
|
||||
hass.loop,
|
||||
websession,
|
||||
host,
|
||||
cam_config[CONF_PORT],
|
||||
username=username,
|
||||
password=password,
|
||||
timeout=cam_config[CONF_TIMEOUT],
|
||||
)
|
||||
|
||||
if switches is None:
|
||||
switches = [setting for setting in cam.enabled_settings
|
||||
if setting in SWITCHES]
|
||||
switches = [
|
||||
setting for setting in cam.enabled_settings if setting in SWITCHES
|
||||
]
|
||||
|
||||
if sensors is None:
|
||||
sensors = [sensor for sensor in cam.enabled_sensors
|
||||
if sensor in SENSORS]
|
||||
sensors.extend(['audio_connections', 'video_connections'])
|
||||
sensors = [sensor for sensor in cam.enabled_sensors if sensor in SENSORS]
|
||||
sensors.extend(["audio_connections", "video_connections"])
|
||||
|
||||
if motion is None:
|
||||
motion = 'motion_active' in cam.enabled_sensors
|
||||
motion = "motion_active" in cam.enabled_sensors
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update_data(now):
|
||||
|
|
@ -194,8 +240,7 @@ def async_setup(hass, config):
|
|||
yield from cam.update()
|
||||
async_dispatcher_send(hass, SIGNAL_UPDATE_DATA, host)
|
||||
|
||||
async_track_point_in_utc_time(
|
||||
hass, async_update_data, utcnow() + interval)
|
||||
async_track_point_in_utc_time(hass, async_update_data, utcnow() + interval)
|
||||
|
||||
yield from async_update_data(None)
|
||||
|
||||
|
|
@ -203,42 +248,50 @@ def async_setup(hass, config):
|
|||
webcams[host] = cam
|
||||
|
||||
mjpeg_camera = {
|
||||
CONF_PLATFORM: 'mjpeg',
|
||||
CONF_PLATFORM: "mjpeg",
|
||||
CONF_MJPEG_URL: cam.mjpeg_url,
|
||||
CONF_STILL_IMAGE_URL: cam.image_url,
|
||||
CONF_NAME: name,
|
||||
}
|
||||
if username and password:
|
||||
mjpeg_camera.update({
|
||||
CONF_USERNAME: username,
|
||||
CONF_PASSWORD: password
|
||||
})
|
||||
mjpeg_camera.update({CONF_USERNAME: username, CONF_PASSWORD: password})
|
||||
|
||||
hass.async_create_task(discovery.async_load_platform(
|
||||
hass, 'camera', 'mjpeg', mjpeg_camera, config))
|
||||
hass.async_create_task(
|
||||
discovery.async_load_platform(hass, "camera", "mjpeg", mjpeg_camera, config)
|
||||
)
|
||||
|
||||
if sensors:
|
||||
hass.async_create_task(discovery.async_load_platform(
|
||||
hass, 'sensor', DOMAIN, {
|
||||
CONF_NAME: name,
|
||||
CONF_HOST: host,
|
||||
CONF_SENSORS: sensors,
|
||||
}, config))
|
||||
hass.async_create_task(
|
||||
discovery.async_load_platform(
|
||||
hass,
|
||||
"sensor",
|
||||
DOMAIN,
|
||||
{CONF_NAME: name, CONF_HOST: host, CONF_SENSORS: sensors},
|
||||
config,
|
||||
)
|
||||
)
|
||||
|
||||
if switches:
|
||||
hass.async_create_task(discovery.async_load_platform(
|
||||
hass, 'switch', DOMAIN, {
|
||||
CONF_NAME: name,
|
||||
CONF_HOST: host,
|
||||
CONF_SWITCHES: switches,
|
||||
}, config))
|
||||
hass.async_create_task(
|
||||
discovery.async_load_platform(
|
||||
hass,
|
||||
"switch",
|
||||
DOMAIN,
|
||||
{CONF_NAME: name, CONF_HOST: host, CONF_SWITCHES: switches},
|
||||
config,
|
||||
)
|
||||
)
|
||||
|
||||
if motion:
|
||||
hass.async_create_task(discovery.async_load_platform(
|
||||
hass, 'binary_sensor', DOMAIN, {
|
||||
CONF_HOST: host,
|
||||
CONF_NAME: name,
|
||||
}, config))
|
||||
hass.async_create_task(
|
||||
discovery.async_load_platform(
|
||||
hass,
|
||||
"binary_sensor",
|
||||
DOMAIN,
|
||||
{CONF_HOST: host, CONF_NAME: name},
|
||||
config,
|
||||
)
|
||||
)
|
||||
|
||||
tasks = [async_setup_ipcamera(conf) for conf in config[DOMAIN]]
|
||||
if tasks:
|
||||
|
|
@ -258,6 +311,7 @@ class AndroidIPCamEntity(Entity):
|
|||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Register update dispatcher."""
|
||||
|
||||
@callback
|
||||
def async_ipcam_update(host):
|
||||
"""Update callback."""
|
||||
|
|
@ -265,8 +319,7 @@ class AndroidIPCamEntity(Entity):
|
|||
return
|
||||
self.async_schedule_update_ha_state(True)
|
||||
|
||||
async_dispatcher_connect(
|
||||
self.hass, SIGNAL_UPDATE_DATA, async_ipcam_update)
|
||||
async_dispatcher_connect(self.hass, SIGNAL_UPDATE_DATA, async_ipcam_update)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
|
|
@ -285,9 +338,7 @@ class AndroidIPCamEntity(Entity):
|
|||
if self._ipcam.status_data is None:
|
||||
return state_attr
|
||||
|
||||
state_attr[ATTR_VID_CONNS] = \
|
||||
self._ipcam.status_data.get('video_connections')
|
||||
state_attr[ATTR_AUD_CONNS] = \
|
||||
self._ipcam.status_data.get('audio_connections')
|
||||
state_attr[ATTR_VID_CONNS] = self._ipcam.status_data.get("video_connections")
|
||||
state_attr[ATTR_AUD_CONNS] = self._ipcam.status_data.get("audio_connections")
|
||||
|
||||
return state_attr
|
||||
|
|
|
|||
|
|
@ -9,33 +9,38 @@ from datetime import timedelta
|
|||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (CONF_HOST, CONF_PORT)
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
REQUIREMENTS = ['apcaccess==0.0.13']
|
||||
REQUIREMENTS = ["apcaccess==0.0.13"]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_TYPE = 'type'
|
||||
CONF_TYPE = "type"
|
||||
|
||||
DATA = None
|
||||
DEFAULT_HOST = 'localhost'
|
||||
DEFAULT_HOST = "localhost"
|
||||
DEFAULT_PORT = 3551
|
||||
DOMAIN = 'apcupsd'
|
||||
DOMAIN = "apcupsd"
|
||||
|
||||
KEY_STATUS = 'STATUS'
|
||||
KEY_STATUS = "STATUS"
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
|
||||
|
||||
VALUE_ONLINE = 'ONLINE'
|
||||
VALUE_ONLINE = "ONLINE"
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
DOMAIN: vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
}
|
||||
)
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
|
|
@ -68,6 +73,7 @@ class APCUPSdData:
|
|||
def __init__(self, host, port):
|
||||
"""Initialize the data object."""
|
||||
from apcaccess import status
|
||||
|
||||
self._host = host
|
||||
self._port = port
|
||||
self._status = None
|
||||
|
|
|
|||
|
|
@ -14,11 +14,25 @@ import async_timeout
|
|||
from homeassistant.bootstrap import DATA_LOGGING
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_STOP, EVENT_TIME_CHANGED, HTTP_BAD_REQUEST,
|
||||
HTTP_CREATED, HTTP_NOT_FOUND, MATCH_ALL, URL_API, URL_API_COMPONENTS,
|
||||
URL_API_CONFIG, URL_API_DISCOVERY_INFO, URL_API_ERROR_LOG, URL_API_EVENTS,
|
||||
URL_API_SERVICES, URL_API_STATES, URL_API_STATES_ENTITY, URL_API_STREAM,
|
||||
URL_API_TEMPLATE, __version__)
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
EVENT_TIME_CHANGED,
|
||||
HTTP_BAD_REQUEST,
|
||||
HTTP_CREATED,
|
||||
HTTP_NOT_FOUND,
|
||||
MATCH_ALL,
|
||||
URL_API,
|
||||
URL_API_COMPONENTS,
|
||||
URL_API_CONFIG,
|
||||
URL_API_DISCOVERY_INFO,
|
||||
URL_API_ERROR_LOG,
|
||||
URL_API_EVENTS,
|
||||
URL_API_SERVICES,
|
||||
URL_API_STATES,
|
||||
URL_API_STATES_ENTITY,
|
||||
URL_API_STREAM,
|
||||
URL_API_TEMPLATE,
|
||||
__version__,
|
||||
)
|
||||
import homeassistant.core as ha
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.helpers import template
|
||||
|
|
@ -28,15 +42,15 @@ from homeassistant.helpers.json import JSONEncoder
|
|||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_BASE_URL = 'base_url'
|
||||
ATTR_LOCATION_NAME = 'location_name'
|
||||
ATTR_REQUIRES_API_PASSWORD = 'requires_api_password'
|
||||
ATTR_VERSION = 'version'
|
||||
ATTR_BASE_URL = "base_url"
|
||||
ATTR_LOCATION_NAME = "location_name"
|
||||
ATTR_REQUIRES_API_PASSWORD = "requires_api_password"
|
||||
ATTR_VERSION = "version"
|
||||
|
||||
DOMAIN = 'api'
|
||||
DEPENDENCIES = ['http']
|
||||
DOMAIN = "api"
|
||||
DEPENDENCIES = ["http"]
|
||||
|
||||
STREAM_PING_PAYLOAD = 'ping'
|
||||
STREAM_PING_PAYLOAD = "ping"
|
||||
STREAM_PING_INTERVAL = 50 # seconds
|
||||
|
||||
|
||||
|
|
@ -65,7 +79,7 @@ class APIStatusView(HomeAssistantView):
|
|||
"""View to handle Status requests."""
|
||||
|
||||
url = URL_API
|
||||
name = 'api:status'
|
||||
name = "api:status"
|
||||
|
||||
@ha.callback
|
||||
def get(self, request):
|
||||
|
|
@ -77,17 +91,17 @@ class APIEventStream(HomeAssistantView):
|
|||
"""View to handle EventStream requests."""
|
||||
|
||||
url = URL_API_STREAM
|
||||
name = 'api:stream'
|
||||
name = "api:stream"
|
||||
|
||||
async def get(self, request):
|
||||
"""Provide a streaming interface for the event bus."""
|
||||
hass = request.app['hass']
|
||||
hass = request.app["hass"]
|
||||
stop_obj = object()
|
||||
to_write = asyncio.Queue(loop=hass.loop)
|
||||
|
||||
restrict = request.query.get('restrict')
|
||||
restrict = request.query.get("restrict")
|
||||
if restrict:
|
||||
restrict = restrict.split(',') + [EVENT_HOMEASSISTANT_STOP]
|
||||
restrict = restrict.split(",") + [EVENT_HOMEASSISTANT_STOP]
|
||||
|
||||
async def forward_events(event):
|
||||
"""Forward events to the open request."""
|
||||
|
|
@ -107,7 +121,7 @@ class APIEventStream(HomeAssistantView):
|
|||
await to_write.put(data)
|
||||
|
||||
response = web.StreamResponse()
|
||||
response.content_type = 'text/event-stream'
|
||||
response.content_type = "text/event-stream"
|
||||
await response.prepare(request)
|
||||
|
||||
unsub_stream = hass.bus.async_listen(MATCH_ALL, forward_events)
|
||||
|
|
@ -120,17 +134,15 @@ class APIEventStream(HomeAssistantView):
|
|||
|
||||
while True:
|
||||
try:
|
||||
with async_timeout.timeout(STREAM_PING_INTERVAL,
|
||||
loop=hass.loop):
|
||||
with async_timeout.timeout(STREAM_PING_INTERVAL, loop=hass.loop):
|
||||
payload = await to_write.get()
|
||||
|
||||
if payload is stop_obj:
|
||||
break
|
||||
|
||||
msg = "data: {}\n\n".format(payload)
|
||||
_LOGGER.debug(
|
||||
"STREAM %s WRITING %s", id(stop_obj), msg.strip())
|
||||
await response.write(msg.encode('UTF-8'))
|
||||
_LOGGER.debug("STREAM %s WRITING %s", id(stop_obj), msg.strip())
|
||||
await response.write(msg.encode("UTF-8"))
|
||||
except asyncio.TimeoutError:
|
||||
await to_write.put(STREAM_PING_PAYLOAD)
|
||||
|
||||
|
|
@ -146,12 +158,12 @@ class APIConfigView(HomeAssistantView):
|
|||
"""View to handle Configuration requests."""
|
||||
|
||||
url = URL_API_CONFIG
|
||||
name = 'api:config'
|
||||
name = "api:config"
|
||||
|
||||
@ha.callback
|
||||
def get(self, request):
|
||||
"""Get current configuration."""
|
||||
return self.json(request.app['hass'].config.as_dict())
|
||||
return self.json(request.app["hass"].config.as_dict())
|
||||
|
||||
|
||||
class APIDiscoveryView(HomeAssistantView):
|
||||
|
|
@ -159,19 +171,21 @@ class APIDiscoveryView(HomeAssistantView):
|
|||
|
||||
requires_auth = False
|
||||
url = URL_API_DISCOVERY_INFO
|
||||
name = 'api:discovery'
|
||||
name = "api:discovery"
|
||||
|
||||
@ha.callback
|
||||
def get(self, request):
|
||||
"""Get discovery information."""
|
||||
hass = request.app['hass']
|
||||
hass = request.app["hass"]
|
||||
needs_auth = hass.config.api.api_password is not None
|
||||
return self.json({
|
||||
return self.json(
|
||||
{
|
||||
ATTR_BASE_URL: hass.config.api.base_url,
|
||||
ATTR_LOCATION_NAME: hass.config.location_name,
|
||||
ATTR_REQUIRES_API_PASSWORD: needs_auth,
|
||||
ATTR_VERSION: __version__,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class APIStatesView(HomeAssistantView):
|
||||
|
|
@ -183,58 +197,58 @@ class APIStatesView(HomeAssistantView):
|
|||
@ha.callback
|
||||
def get(self, request):
|
||||
"""Get current states."""
|
||||
return self.json(request.app['hass'].states.async_all())
|
||||
return self.json(request.app["hass"].states.async_all())
|
||||
|
||||
|
||||
class APIEntityStateView(HomeAssistantView):
|
||||
"""View to handle EntityState requests."""
|
||||
|
||||
url = '/api/states/{entity_id}'
|
||||
name = 'api:entity-state'
|
||||
url = "/api/states/{entity_id}"
|
||||
name = "api:entity-state"
|
||||
|
||||
@ha.callback
|
||||
def get(self, request, entity_id):
|
||||
"""Retrieve state of entity."""
|
||||
state = request.app['hass'].states.get(entity_id)
|
||||
state = request.app["hass"].states.get(entity_id)
|
||||
if state:
|
||||
return self.json(state)
|
||||
return self.json_message("Entity not found.", HTTP_NOT_FOUND)
|
||||
|
||||
async def post(self, request, entity_id):
|
||||
"""Update state of entity."""
|
||||
hass = request.app['hass']
|
||||
hass = request.app["hass"]
|
||||
try:
|
||||
data = await request.json()
|
||||
except ValueError:
|
||||
return self.json_message(
|
||||
"Invalid JSON specified.", HTTP_BAD_REQUEST)
|
||||
return self.json_message("Invalid JSON specified.", HTTP_BAD_REQUEST)
|
||||
|
||||
new_state = data.get('state')
|
||||
new_state = data.get("state")
|
||||
|
||||
if new_state is None:
|
||||
return self.json_message("No state specified.", HTTP_BAD_REQUEST)
|
||||
|
||||
attributes = data.get('attributes')
|
||||
force_update = data.get('force_update', False)
|
||||
attributes = data.get("attributes")
|
||||
force_update = data.get("force_update", False)
|
||||
|
||||
is_new_state = hass.states.get(entity_id) is None
|
||||
|
||||
# Write state
|
||||
hass.states.async_set(entity_id, new_state, attributes, force_update,
|
||||
self.context(request))
|
||||
hass.states.async_set(
|
||||
entity_id, new_state, attributes, force_update, self.context(request)
|
||||
)
|
||||
|
||||
# Read the state back for our response
|
||||
status_code = HTTP_CREATED if is_new_state else 200
|
||||
resp = self.json(hass.states.get(entity_id), status_code)
|
||||
|
||||
resp.headers.add('Location', URL_API_STATES_ENTITY.format(entity_id))
|
||||
resp.headers.add("Location", URL_API_STATES_ENTITY.format(entity_id))
|
||||
|
||||
return resp
|
||||
|
||||
@ha.callback
|
||||
def delete(self, request, entity_id):
|
||||
"""Remove entity."""
|
||||
if request.app['hass'].states.async_remove(entity_id):
|
||||
if request.app["hass"].states.async_remove(entity_id):
|
||||
return self.json_message("Entity removed.")
|
||||
return self.json_message("Entity not found.", HTTP_NOT_FOUND)
|
||||
|
||||
|
|
@ -243,19 +257,19 @@ class APIEventListenersView(HomeAssistantView):
|
|||
"""View to handle EventListeners requests."""
|
||||
|
||||
url = URL_API_EVENTS
|
||||
name = 'api:event-listeners'
|
||||
name = "api:event-listeners"
|
||||
|
||||
@ha.callback
|
||||
def get(self, request):
|
||||
"""Get event listeners."""
|
||||
return self.json(async_events_json(request.app['hass']))
|
||||
return self.json(async_events_json(request.app["hass"]))
|
||||
|
||||
|
||||
class APIEventView(HomeAssistantView):
|
||||
"""View to handle Event requests."""
|
||||
|
||||
url = '/api/events/{event_type}'
|
||||
name = 'api:event'
|
||||
url = "/api/events/{event_type}"
|
||||
name = "api:event"
|
||||
|
||||
async def post(self, request, event_type):
|
||||
"""Fire events."""
|
||||
|
|
@ -264,24 +278,26 @@ class APIEventView(HomeAssistantView):
|
|||
event_data = json.loads(body) if body else None
|
||||
except ValueError:
|
||||
return self.json_message(
|
||||
"Event data should be valid JSON.", HTTP_BAD_REQUEST)
|
||||
"Event data should be valid JSON.", HTTP_BAD_REQUEST
|
||||
)
|
||||
|
||||
if event_data is not None and not isinstance(event_data, dict):
|
||||
return self.json_message(
|
||||
"Event data should be a JSON object", HTTP_BAD_REQUEST)
|
||||
"Event data should be a JSON object", HTTP_BAD_REQUEST
|
||||
)
|
||||
|
||||
# Special case handling for event STATE_CHANGED
|
||||
# We will try to convert state dicts back to State objects
|
||||
if event_type == ha.EVENT_STATE_CHANGED and event_data:
|
||||
for key in ('old_state', 'new_state'):
|
||||
for key in ("old_state", "new_state"):
|
||||
state = ha.State.from_dict(event_data.get(key))
|
||||
|
||||
if state:
|
||||
event_data[key] = state
|
||||
|
||||
request.app['hass'].bus.async_fire(
|
||||
event_type, event_data, ha.EventOrigin.remote,
|
||||
self.context(request))
|
||||
request.app["hass"].bus.async_fire(
|
||||
event_type, event_data, ha.EventOrigin.remote, self.context(request)
|
||||
)
|
||||
|
||||
return self.json_message("Event {} fired.".format(event_type))
|
||||
|
||||
|
|
@ -290,36 +306,36 @@ class APIServicesView(HomeAssistantView):
|
|||
"""View to handle Services requests."""
|
||||
|
||||
url = URL_API_SERVICES
|
||||
name = 'api:services'
|
||||
name = "api:services"
|
||||
|
||||
async def get(self, request):
|
||||
"""Get registered services."""
|
||||
services = await async_services_json(request.app['hass'])
|
||||
services = await async_services_json(request.app["hass"])
|
||||
return self.json(services)
|
||||
|
||||
|
||||
class APIDomainServicesView(HomeAssistantView):
|
||||
"""View to handle DomainServices requests."""
|
||||
|
||||
url = '/api/services/{domain}/{service}'
|
||||
name = 'api:domain-services'
|
||||
url = "/api/services/{domain}/{service}"
|
||||
name = "api:domain-services"
|
||||
|
||||
async def post(self, request, domain, service):
|
||||
"""Call a service.
|
||||
|
||||
Returns a list of changed states.
|
||||
"""
|
||||
hass = request.app['hass']
|
||||
hass = request.app["hass"]
|
||||
body = await request.text()
|
||||
try:
|
||||
data = json.loads(body) if body else None
|
||||
except ValueError:
|
||||
return self.json_message(
|
||||
"Data should be valid JSON.", HTTP_BAD_REQUEST)
|
||||
return self.json_message("Data should be valid JSON.", HTTP_BAD_REQUEST)
|
||||
|
||||
with AsyncTrackStates(hass) as changed_states:
|
||||
await hass.services.async_call(
|
||||
domain, service, data, True, self.context(request))
|
||||
domain, service, data, True, self.context(request)
|
||||
)
|
||||
|
||||
return self.json(changed_states)
|
||||
|
||||
|
|
@ -328,50 +344,52 @@ class APIComponentsView(HomeAssistantView):
|
|||
"""View to handle Components requests."""
|
||||
|
||||
url = URL_API_COMPONENTS
|
||||
name = 'api:components'
|
||||
name = "api:components"
|
||||
|
||||
@ha.callback
|
||||
def get(self, request):
|
||||
"""Get current loaded components."""
|
||||
return self.json(request.app['hass'].config.components)
|
||||
return self.json(request.app["hass"].config.components)
|
||||
|
||||
|
||||
class APITemplateView(HomeAssistantView):
|
||||
"""View to handle Template requests."""
|
||||
|
||||
url = URL_API_TEMPLATE
|
||||
name = 'api:template'
|
||||
name = "api:template"
|
||||
|
||||
async def post(self, request):
|
||||
"""Render a template."""
|
||||
try:
|
||||
data = await request.json()
|
||||
tpl = template.Template(data['template'], request.app['hass'])
|
||||
return tpl.async_render(data.get('variables'))
|
||||
tpl = template.Template(data["template"], request.app["hass"])
|
||||
return tpl.async_render(data.get("variables"))
|
||||
except (ValueError, TemplateError) as ex:
|
||||
return self.json_message(
|
||||
"Error rendering template: {}".format(ex), HTTP_BAD_REQUEST)
|
||||
"Error rendering template: {}".format(ex), HTTP_BAD_REQUEST
|
||||
)
|
||||
|
||||
|
||||
class APIErrorLog(HomeAssistantView):
|
||||
"""View to fetch the API error log."""
|
||||
|
||||
url = URL_API_ERROR_LOG
|
||||
name = 'api:error_log'
|
||||
name = "api:error_log"
|
||||
|
||||
async def get(self, request):
|
||||
"""Retrieve API error log."""
|
||||
return web.FileResponse(request.app['hass'].data[DATA_LOGGING])
|
||||
return web.FileResponse(request.app["hass"].data[DATA_LOGGING])
|
||||
|
||||
|
||||
async def async_services_json(hass):
|
||||
"""Generate services data to JSONify."""
|
||||
descriptions = await async_get_all_descriptions(hass)
|
||||
return [{'domain': key, 'services': value}
|
||||
for key, value in descriptions.items()]
|
||||
return [{"domain": key, "services": value} for key, value in descriptions.items()]
|
||||
|
||||
|
||||
def async_events_json(hass):
|
||||
"""Generate event data to JSONify."""
|
||||
return [{'event': key, 'listener_count': value}
|
||||
for key, value in hass.bus.async_listeners().items()]
|
||||
return [
|
||||
{"event": key, "listener_count": value}
|
||||
for key, value in hass.bus.async_listeners().items()
|
||||
]
|
||||
|
|
|
|||
|
|
@ -16,35 +16,35 @@ from homeassistant.helpers import discovery
|
|||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['pyatv==0.3.10']
|
||||
REQUIREMENTS = ["pyatv==0.3.10"]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'apple_tv'
|
||||
DOMAIN = "apple_tv"
|
||||
|
||||
SERVICE_SCAN = 'apple_tv_scan'
|
||||
SERVICE_AUTHENTICATE = 'apple_tv_authenticate'
|
||||
SERVICE_SCAN = "apple_tv_scan"
|
||||
SERVICE_AUTHENTICATE = "apple_tv_authenticate"
|
||||
|
||||
ATTR_ATV = 'atv'
|
||||
ATTR_POWER = 'power'
|
||||
ATTR_ATV = "atv"
|
||||
ATTR_POWER = "power"
|
||||
|
||||
CONF_LOGIN_ID = 'login_id'
|
||||
CONF_START_OFF = 'start_off'
|
||||
CONF_CREDENTIALS = 'credentials'
|
||||
CONF_LOGIN_ID = "login_id"
|
||||
CONF_START_OFF = "start_off"
|
||||
CONF_CREDENTIALS = "credentials"
|
||||
|
||||
DEFAULT_NAME = 'Apple TV'
|
||||
DEFAULT_NAME = "Apple TV"
|
||||
|
||||
DATA_APPLE_TV = 'data_apple_tv'
|
||||
DATA_ENTITIES = 'data_apple_tv_entities'
|
||||
DATA_APPLE_TV = "data_apple_tv"
|
||||
DATA_ENTITIES = "data_apple_tv_entities"
|
||||
|
||||
KEY_CONFIG = 'apple_tv_configuring'
|
||||
KEY_CONFIG = "apple_tv_configuring"
|
||||
|
||||
NOTIFICATION_AUTH_ID = 'apple_tv_auth_notification'
|
||||
NOTIFICATION_AUTH_TITLE = 'Apple TV Authentication'
|
||||
NOTIFICATION_SCAN_ID = 'apple_tv_scan_notification'
|
||||
NOTIFICATION_SCAN_TITLE = 'Apple TV Scan'
|
||||
NOTIFICATION_AUTH_ID = "apple_tv_auth_notification"
|
||||
NOTIFICATION_AUTH_TITLE = "Apple TV Authentication"
|
||||
NOTIFICATION_SCAN_ID = "apple_tv_scan_notification"
|
||||
NOTIFICATION_SCAN_TITLE = "Apple TV Scan"
|
||||
|
||||
T = TypeVar('T') # pylint: disable=invalid-name
|
||||
T = TypeVar("T") # pylint: disable=invalid-name
|
||||
|
||||
|
||||
# This version of ensure_list interprets an empty dict as no value
|
||||
|
|
@ -55,22 +55,30 @@ def ensure_list(value: Union[T, Sequence[T]]) -> Sequence[T]:
|
|||
return value if isinstance(value, list) else [value]
|
||||
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.All(ensure_list, [vol.Schema({
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
DOMAIN: vol.All(
|
||||
ensure_list,
|
||||
[
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_LOGIN_ID): cv.string,
|
||||
vol.Optional(CONF_CREDENTIALS): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_START_OFF, default=False): cv.boolean,
|
||||
})])
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
}
|
||||
)
|
||||
],
|
||||
)
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
# Currently no attributes but it might change later
|
||||
APPLE_TV_SCAN_SCHEMA = vol.Schema({})
|
||||
|
||||
APPLE_TV_AUTHENTICATE_SCHEMA = vol.Schema({
|
||||
ATTR_ENTITY_ID: cv.entity_ids,
|
||||
})
|
||||
APPLE_TV_AUTHENTICATE_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids})
|
||||
|
||||
|
||||
def request_configuration(hass, config, atv, credentials):
|
||||
|
|
@ -81,30 +89,34 @@ def request_configuration(hass, config, atv, credentials):
|
|||
def configuration_callback(callback_data):
|
||||
"""Handle the submitted configuration."""
|
||||
from pyatv import exceptions
|
||||
pin = callback_data.get('pin')
|
||||
|
||||
pin = callback_data.get("pin")
|
||||
|
||||
try:
|
||||
yield from atv.airplay.finish_authentication(pin)
|
||||
hass.components.persistent_notification.async_create(
|
||||
'Authentication succeeded!<br /><br />Add the following '
|
||||
'to credentials: in your apple_tv configuration:<br /><br />'
|
||||
'{0}'.format(credentials),
|
||||
"Authentication succeeded!<br /><br />Add the following "
|
||||
"to credentials: in your apple_tv configuration:<br /><br />"
|
||||
"{0}".format(credentials),
|
||||
title=NOTIFICATION_AUTH_TITLE,
|
||||
notification_id=NOTIFICATION_AUTH_ID)
|
||||
notification_id=NOTIFICATION_AUTH_ID,
|
||||
)
|
||||
except exceptions.DeviceAuthenticationError as ex:
|
||||
hass.components.persistent_notification.async_create(
|
||||
'Authentication failed! Did you enter correct PIN?<br /><br />'
|
||||
'Details: {0}'.format(ex),
|
||||
"Authentication failed! Did you enter correct PIN?<br /><br />"
|
||||
"Details: {0}".format(ex),
|
||||
title=NOTIFICATION_AUTH_TITLE,
|
||||
notification_id=NOTIFICATION_AUTH_ID)
|
||||
notification_id=NOTIFICATION_AUTH_ID,
|
||||
)
|
||||
|
||||
hass.async_add_job(configurator.request_done, instance)
|
||||
|
||||
instance = configurator.request_config(
|
||||
'Apple TV Authentication', configuration_callback,
|
||||
description='Please enter PIN code shown on screen.',
|
||||
submit_caption='Confirm',
|
||||
fields=[{'id': 'pin', 'name': 'PIN Code', 'type': 'password'}]
|
||||
"Apple TV Authentication",
|
||||
configuration_callback,
|
||||
description="Please enter PIN code shown on screen.",
|
||||
submit_caption="Confirm",
|
||||
fields=[{"id": "pin", "name": "PIN Code", "type": "password"}],
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -112,24 +124,28 @@ def request_configuration(hass, config, atv, credentials):
|
|||
def scan_for_apple_tvs(hass):
|
||||
"""Scan for devices and present a notification of the ones found."""
|
||||
import pyatv
|
||||
|
||||
atvs = yield from pyatv.scan_for_apple_tvs(hass.loop, timeout=3)
|
||||
|
||||
devices = []
|
||||
for atv in atvs:
|
||||
login_id = atv.login_id
|
||||
if login_id is None:
|
||||
login_id = 'Home Sharing disabled'
|
||||
devices.append('Name: {0}<br />Host: {1}<br />Login ID: {2}'.format(
|
||||
atv.name, atv.address, login_id))
|
||||
login_id = "Home Sharing disabled"
|
||||
devices.append(
|
||||
"Name: {0}<br />Host: {1}<br />Login ID: {2}".format(
|
||||
atv.name, atv.address, login_id
|
||||
)
|
||||
)
|
||||
|
||||
if not devices:
|
||||
devices = ['No device(s) found']
|
||||
devices = ["No device(s) found"]
|
||||
|
||||
hass.components.persistent_notification.async_create(
|
||||
'The following devices were found:<br /><br />' +
|
||||
'<br /><br />'.join(devices),
|
||||
"The following devices were found:<br /><br />" + "<br /><br />".join(devices),
|
||||
title=NOTIFICATION_SCAN_TITLE,
|
||||
notification_id=NOTIFICATION_SCAN_ID)
|
||||
notification_id=NOTIFICATION_SCAN_ID,
|
||||
)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
|
|
@ -148,8 +164,11 @@ def async_setup(hass, config):
|
|||
return
|
||||
|
||||
if entity_ids:
|
||||
devices = [device for device in hass.data[DATA_ENTITIES]
|
||||
if device.entity_id in entity_ids]
|
||||
devices = [
|
||||
device
|
||||
for device in hass.data[DATA_ENTITIES]
|
||||
if device.entity_id in entity_ids
|
||||
]
|
||||
else:
|
||||
devices = hass.data[DATA_ENTITIES]
|
||||
|
||||
|
|
@ -160,20 +179,22 @@ def async_setup(hass, config):
|
|||
atv = device.atv
|
||||
credentials = yield from atv.airplay.generate_credentials()
|
||||
yield from atv.airplay.load_credentials(credentials)
|
||||
_LOGGER.debug('Generated new credentials: %s', credentials)
|
||||
_LOGGER.debug("Generated new credentials: %s", credentials)
|
||||
yield from atv.airplay.start_authentication()
|
||||
hass.async_add_job(request_configuration,
|
||||
hass, config, atv, credentials)
|
||||
hass.async_add_job(request_configuration, hass, config, atv, credentials)
|
||||
|
||||
@asyncio.coroutine
|
||||
def atv_discovered(service, info):
|
||||
"""Set up an Apple TV that was auto discovered."""
|
||||
yield from _setup_atv(hass, {
|
||||
CONF_NAME: info['name'],
|
||||
CONF_HOST: info['host'],
|
||||
CONF_LOGIN_ID: info['properties']['hG'],
|
||||
CONF_START_OFF: False
|
||||
})
|
||||
yield from _setup_atv(
|
||||
hass,
|
||||
{
|
||||
CONF_NAME: info["name"],
|
||||
CONF_HOST: info["host"],
|
||||
CONF_LOGIN_ID: info["properties"]["hG"],
|
||||
CONF_START_OFF: False,
|
||||
},
|
||||
)
|
||||
|
||||
discovery.async_listen(hass, SERVICE_APPLE_TV, atv_discovered)
|
||||
|
||||
|
|
@ -182,12 +203,15 @@ def async_setup(hass, config):
|
|||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SCAN, async_service_handler,
|
||||
schema=APPLE_TV_SCAN_SCHEMA)
|
||||
DOMAIN, SERVICE_SCAN, async_service_handler, schema=APPLE_TV_SCAN_SCHEMA
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_AUTHENTICATE, async_service_handler,
|
||||
schema=APPLE_TV_AUTHENTICATE_SCHEMA)
|
||||
DOMAIN,
|
||||
SERVICE_AUTHENTICATE,
|
||||
async_service_handler,
|
||||
schema=APPLE_TV_AUTHENTICATE_SCHEMA,
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
|
@ -196,6 +220,7 @@ def async_setup(hass, config):
|
|||
def _setup_atv(hass, atv_config):
|
||||
"""Set up an Apple TV."""
|
||||
import pyatv
|
||||
|
||||
name = atv_config.get(CONF_NAME)
|
||||
host = atv_config.get(CONF_HOST)
|
||||
login_id = atv_config.get(CONF_LOGIN_ID)
|
||||
|
|
@ -212,16 +237,15 @@ def _setup_atv(hass, atv_config):
|
|||
yield from atv.airplay.load_credentials(credentials)
|
||||
|
||||
power = AppleTVPowerManager(hass, atv, start_off)
|
||||
hass.data[DATA_APPLE_TV][host] = {
|
||||
ATTR_ATV: atv,
|
||||
ATTR_POWER: power
|
||||
}
|
||||
hass.data[DATA_APPLE_TV][host] = {ATTR_ATV: atv, ATTR_POWER: power}
|
||||
|
||||
hass.async_create_task(discovery.async_load_platform(
|
||||
hass, 'media_player', DOMAIN, atv_config))
|
||||
hass.async_create_task(
|
||||
discovery.async_load_platform(hass, "media_player", DOMAIN, atv_config)
|
||||
)
|
||||
|
||||
hass.async_create_task(discovery.async_load_platform(
|
||||
hass, 'remote', DOMAIN, atv_config))
|
||||
hass.async_create_task(
|
||||
discovery.async_load_platform(hass, "remote", DOMAIN, atv_config)
|
||||
)
|
||||
|
||||
|
||||
class AppleTVPowerManager:
|
||||
|
|
|
|||
|
|
@ -8,24 +8,21 @@ import logging
|
|||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.const import CONF_PORT
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['PyMata==2.14']
|
||||
REQUIREMENTS = ["PyMata==2.14"]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
BOARD = None
|
||||
|
||||
DOMAIN = 'arduino'
|
||||
DOMAIN = "arduino"
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_PORT): cv.string,
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{DOMAIN: vol.Schema({vol.Required(CONF_PORT): cv.string})}, extra=vol.ALLOW_EXTRA
|
||||
)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
|
|
@ -46,8 +43,10 @@ def setup(hass, config):
|
|||
_LOGGER.error("The StandardFirmata sketch should be 2.2 or newer")
|
||||
return False
|
||||
except IndexError:
|
||||
_LOGGER.warning("The version of the StandardFirmata sketch was not"
|
||||
"detected. This may lead to side effects")
|
||||
_LOGGER.warning(
|
||||
"The version of the StandardFirmata sketch was not"
|
||||
"detected. This may lead to side effects"
|
||||
)
|
||||
|
||||
def stop_arduino(event):
|
||||
"""Stop the Arduino service."""
|
||||
|
|
@ -68,26 +67,22 @@ class ArduinoBoard:
|
|||
def __init__(self, port):
|
||||
"""Initialize the board."""
|
||||
from PyMata.pymata import PyMata
|
||||
|
||||
self._port = port
|
||||
self._board = PyMata(self._port, verbose=False)
|
||||
|
||||
def set_mode(self, pin, direction, mode):
|
||||
"""Set the mode and the direction of a given pin."""
|
||||
if mode == 'analog' and direction == 'in':
|
||||
self._board.set_pin_mode(
|
||||
pin, self._board.INPUT, self._board.ANALOG)
|
||||
elif mode == 'analog' and direction == 'out':
|
||||
self._board.set_pin_mode(
|
||||
pin, self._board.OUTPUT, self._board.ANALOG)
|
||||
elif mode == 'digital' and direction == 'in':
|
||||
self._board.set_pin_mode(
|
||||
pin, self._board.INPUT, self._board.DIGITAL)
|
||||
elif mode == 'digital' and direction == 'out':
|
||||
self._board.set_pin_mode(
|
||||
pin, self._board.OUTPUT, self._board.DIGITAL)
|
||||
elif mode == 'pwm':
|
||||
self._board.set_pin_mode(
|
||||
pin, self._board.OUTPUT, self._board.PWM)
|
||||
if mode == "analog" and direction == "in":
|
||||
self._board.set_pin_mode(pin, self._board.INPUT, self._board.ANALOG)
|
||||
elif mode == "analog" and direction == "out":
|
||||
self._board.set_pin_mode(pin, self._board.OUTPUT, self._board.ANALOG)
|
||||
elif mode == "digital" and direction == "in":
|
||||
self._board.set_pin_mode(pin, self._board.INPUT, self._board.DIGITAL)
|
||||
elif mode == "digital" and direction == "out":
|
||||
self._board.set_pin_mode(pin, self._board.OUTPUT, self._board.DIGITAL)
|
||||
elif mode == "pwm":
|
||||
self._board.set_pin_mode(pin, self._board.OUTPUT, self._board.PWM)
|
||||
|
||||
def get_analog_inputs(self):
|
||||
"""Get the values from the pins."""
|
||||
|
|
|
|||
|
|
@ -11,36 +11,39 @@ import voluptuous as vol
|
|||
from requests.exceptions import HTTPError, ConnectTimeout
|
||||
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.const import (
|
||||
CONF_USERNAME, CONF_PASSWORD, CONF_SCAN_INTERVAL)
|
||||
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_SCAN_INTERVAL
|
||||
from homeassistant.helpers.event import track_time_interval
|
||||
from homeassistant.helpers.dispatcher import dispatcher_send
|
||||
|
||||
REQUIREMENTS = ['pyarlo==0.2.0']
|
||||
REQUIREMENTS = ["pyarlo==0.2.0"]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_ATTRIBUTION = "Data provided by arlo.netgear.com"
|
||||
|
||||
DATA_ARLO = 'data_arlo'
|
||||
DEFAULT_BRAND = 'Netgear Arlo'
|
||||
DOMAIN = 'arlo'
|
||||
DATA_ARLO = "data_arlo"
|
||||
DEFAULT_BRAND = "Netgear Arlo"
|
||||
DOMAIN = "arlo"
|
||||
|
||||
NOTIFICATION_ID = 'arlo_notification'
|
||||
NOTIFICATION_TITLE = 'Arlo Component Setup'
|
||||
NOTIFICATION_ID = "arlo_notification"
|
||||
NOTIFICATION_TITLE = "Arlo Component Setup"
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=60)
|
||||
|
||||
SIGNAL_UPDATE_ARLO = "arlo_update"
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
DOMAIN: vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL):
|
||||
cv.time_period,
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): cv.time_period,
|
||||
}
|
||||
)
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
|
|
@ -58,8 +61,7 @@ def setup(hass, config):
|
|||
return False
|
||||
|
||||
# assign refresh period to base station thread
|
||||
arlo_base_station = next((
|
||||
station for station in arlo.base_stations), None)
|
||||
arlo_base_station = next((station for station in arlo.base_stations), None)
|
||||
|
||||
if arlo_base_station is not None:
|
||||
arlo_base_station.refresh_rate = scan_interval.total_seconds()
|
||||
|
|
@ -72,22 +74,22 @@ def setup(hass, config):
|
|||
except (ConnectTimeout, HTTPError) as ex:
|
||||
_LOGGER.error("Unable to connect to Netgear Arlo: %s", str(ex))
|
||||
hass.components.persistent_notification.create(
|
||||
'Error: {}<br />'
|
||||
'You will need to restart hass after fixing.'
|
||||
''.format(ex),
|
||||
"Error: {}<br />"
|
||||
"You will need to restart hass after fixing."
|
||||
"".format(ex),
|
||||
title=NOTIFICATION_TITLE,
|
||||
notification_id=NOTIFICATION_ID)
|
||||
notification_id=NOTIFICATION_ID,
|
||||
)
|
||||
return False
|
||||
|
||||
def hub_refresh(event_time):
|
||||
"""Call ArloHub to refresh information."""
|
||||
_LOGGER.info("Updating Arlo Hub component")
|
||||
hass.data[DATA_ARLO].update(update_cameras=True,
|
||||
update_base_station=True)
|
||||
hass.data[DATA_ARLO].update(update_cameras=True, update_base_station=True)
|
||||
dispatcher_send(hass, SIGNAL_UPDATE_ARLO)
|
||||
|
||||
# register service
|
||||
hass.services.register(DOMAIN, 'update', hub_refresh)
|
||||
hass.services.register(DOMAIN, "update", hub_refresh)
|
||||
|
||||
# register scan interval for ArloHub
|
||||
track_time_interval(hass, hub_refresh, scan_interval)
|
||||
|
|
|
|||
|
|
@ -13,24 +13,31 @@ from homeassistant.core import callback
|
|||
from homeassistant.helpers import discovery
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_connect, async_dispatcher_send)
|
||||
async_dispatcher_connect,
|
||||
async_dispatcher_send,
|
||||
)
|
||||
|
||||
REQUIREMENTS = ['asterisk_mbox==0.5.0']
|
||||
REQUIREMENTS = ["asterisk_mbox==0.5.0"]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'asterisk_mbox'
|
||||
DOMAIN = "asterisk_mbox"
|
||||
|
||||
SIGNAL_MESSAGE_REQUEST = 'asterisk_mbox.message_request'
|
||||
SIGNAL_MESSAGE_UPDATE = 'asterisk_mbox.message_updated'
|
||||
SIGNAL_MESSAGE_REQUEST = "asterisk_mbox.message_request"
|
||||
SIGNAL_MESSAGE_UPDATE = "asterisk_mbox.message_updated"
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
DOMAIN: vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_PORT): int,
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
}
|
||||
)
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
|
|
@ -43,7 +50,7 @@ def setup(hass, config):
|
|||
|
||||
hass.data[DOMAIN] = AsteriskData(hass, host, port, password)
|
||||
|
||||
discovery.load_platform(hass, 'mailbox', DOMAIN, {}, config)
|
||||
discovery.load_platform(hass, "mailbox", DOMAIN, {}, config)
|
||||
|
||||
return True
|
||||
|
||||
|
|
@ -60,7 +67,8 @@ class AsteriskData:
|
|||
self.messages = []
|
||||
|
||||
async_dispatcher_connect(
|
||||
self.hass, SIGNAL_MESSAGE_REQUEST, self._request_messages)
|
||||
self.hass, SIGNAL_MESSAGE_REQUEST, self._request_messages
|
||||
)
|
||||
|
||||
@callback
|
||||
def handle_data(self, command, msg):
|
||||
|
|
@ -70,9 +78,9 @@ class AsteriskData:
|
|||
if command == CMD_MESSAGE_LIST:
|
||||
_LOGGER.debug("AsteriskVM sent updated message list")
|
||||
self.messages = sorted(
|
||||
msg, key=lambda item: item['info']['origtime'], reverse=True)
|
||||
async_dispatcher_send(
|
||||
self.hass, SIGNAL_MESSAGE_UPDATE, self.messages)
|
||||
msg, key=lambda item: item["info"]["origtime"], reverse=True
|
||||
)
|
||||
async_dispatcher_send(self.hass, SIGNAL_MESSAGE_UPDATE, self.messages)
|
||||
|
||||
@callback
|
||||
def _request_messages(self):
|
||||
|
|
|
|||
|
|
@ -12,8 +12,7 @@ import voluptuous as vol
|
|||
from requests import RequestException
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import (
|
||||
CONF_PASSWORD, CONF_USERNAME, CONF_TIMEOUT)
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, CONF_TIMEOUT
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
|
|
@ -21,40 +20,43 @@ _LOGGER = logging.getLogger(__name__)
|
|||
|
||||
_CONFIGURING = {}
|
||||
|
||||
REQUIREMENTS = ['py-august==0.6.0']
|
||||
REQUIREMENTS = ["py-august==0.6.0"]
|
||||
|
||||
DEFAULT_TIMEOUT = 10
|
||||
ACTIVITY_FETCH_LIMIT = 10
|
||||
ACTIVITY_INITIAL_FETCH_LIMIT = 20
|
||||
|
||||
CONF_LOGIN_METHOD = 'login_method'
|
||||
CONF_INSTALL_ID = 'install_id'
|
||||
CONF_LOGIN_METHOD = "login_method"
|
||||
CONF_INSTALL_ID = "install_id"
|
||||
|
||||
NOTIFICATION_ID = 'august_notification'
|
||||
NOTIFICATION_ID = "august_notification"
|
||||
NOTIFICATION_TITLE = "August Setup"
|
||||
|
||||
AUGUST_CONFIG_FILE = '.august.conf'
|
||||
AUGUST_CONFIG_FILE = ".august.conf"
|
||||
|
||||
DATA_AUGUST = 'august'
|
||||
DOMAIN = 'august'
|
||||
DEFAULT_ENTITY_NAMESPACE = 'august'
|
||||
DATA_AUGUST = "august"
|
||||
DOMAIN = "august"
|
||||
DEFAULT_ENTITY_NAMESPACE = "august"
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5)
|
||||
DEFAULT_SCAN_INTERVAL = timedelta(seconds=5)
|
||||
LOGIN_METHODS = ['phone', 'email']
|
||||
LOGIN_METHODS = ["phone", "email"]
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
DOMAIN: vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_LOGIN_METHOD): vol.In(LOGIN_METHODS),
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_INSTALL_ID): cv.string,
|
||||
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
|
||||
})
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
}
|
||||
)
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
AUGUST_COMPONENTS = [
|
||||
'camera', 'binary_sensor', 'lock'
|
||||
]
|
||||
AUGUST_COMPONENTS = ["camera", "binary_sensor", "lock"]
|
||||
|
||||
|
||||
def request_configuration(hass, config, api, authenticator):
|
||||
|
|
@ -65,12 +67,12 @@ def request_configuration(hass, config, api, authenticator):
|
|||
"""Run when the configuration callback is called."""
|
||||
from august.authenticator import ValidationResult
|
||||
|
||||
result = authenticator.validate_verification_code(
|
||||
data.get('verification_code'))
|
||||
result = authenticator.validate_verification_code(data.get("verification_code"))
|
||||
|
||||
if result == ValidationResult.INVALID_VERIFICATION_CODE:
|
||||
configurator.notify_errors(_CONFIGURING[DOMAIN],
|
||||
"Invalid verification code")
|
||||
configurator.notify_errors(
|
||||
_CONFIGURING[DOMAIN], "Invalid verification code"
|
||||
)
|
||||
elif result == ValidationResult.VALIDATED:
|
||||
setup_august(hass, config, api, authenticator)
|
||||
|
||||
|
|
@ -86,11 +88,10 @@ def request_configuration(hass, config, api, authenticator):
|
|||
august_configuration_callback,
|
||||
description="Please check your {} ({}) and enter the verification "
|
||||
"code below".format(login_method, username),
|
||||
submit_caption='Verify',
|
||||
fields=[{
|
||||
'id': 'verification_code',
|
||||
'name': "Verification code",
|
||||
'type': 'string'}]
|
||||
submit_caption="Verify",
|
||||
fields=[
|
||||
{"id": "verification_code", "name": "Verification code", "type": "string"}
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -109,7 +110,8 @@ def setup_august(hass, config, api, authenticator):
|
|||
"You will need to restart hass after fixing."
|
||||
"".format(ex),
|
||||
title=NOTIFICATION_TITLE,
|
||||
notification_id=NOTIFICATION_ID)
|
||||
notification_id=NOTIFICATION_ID,
|
||||
)
|
||||
|
||||
state = authentication.state
|
||||
|
||||
|
|
@ -146,7 +148,8 @@ def setup(hass, config):
|
|||
conf.get(CONF_USERNAME),
|
||||
conf.get(CONF_PASSWORD),
|
||||
install_id=conf.get(CONF_INSTALL_ID),
|
||||
access_token_cache_file=hass.config.path(AUGUST_CONFIG_FILE))
|
||||
access_token_cache_file=hass.config.path(AUGUST_CONFIG_FILE),
|
||||
)
|
||||
|
||||
return setup_august(hass, config, api, authenticator)
|
||||
|
||||
|
|
@ -200,14 +203,15 @@ class AugustData:
|
|||
def _update_device_activities(self, limit=ACTIVITY_FETCH_LIMIT):
|
||||
"""Update data object with latest from August API."""
|
||||
for house_id in self.house_ids:
|
||||
activities = self._api.get_house_activities(self._access_token,
|
||||
house_id,
|
||||
limit=limit)
|
||||
activities = self._api.get_house_activities(
|
||||
self._access_token, house_id, limit=limit
|
||||
)
|
||||
|
||||
device_ids = {a.device_id for a in activities}
|
||||
for device_id in device_ids:
|
||||
self._activities_by_id[device_id] = [a for a in activities if
|
||||
a.device_id == device_id]
|
||||
self._activities_by_id[device_id] = [
|
||||
a for a in activities if a.device_id == device_id
|
||||
]
|
||||
|
||||
def get_doorbell_detail(self, doorbell_id):
|
||||
"""Return doorbell detail."""
|
||||
|
|
@ -220,7 +224,8 @@ class AugustData:
|
|||
|
||||
for doorbell in self._doorbells:
|
||||
detail_by_id[doorbell.device_id] = self._api.get_doorbell_detail(
|
||||
self._access_token, doorbell.device_id)
|
||||
self._access_token, doorbell.device_id
|
||||
)
|
||||
|
||||
self._doorbell_detail_by_id = detail_by_id
|
||||
|
||||
|
|
@ -241,9 +246,11 @@ class AugustData:
|
|||
|
||||
for lock in self._locks:
|
||||
status_by_id[lock.device_id] = self._api.get_lock_status(
|
||||
self._access_token, lock.device_id)
|
||||
self._access_token, lock.device_id
|
||||
)
|
||||
detail_by_id[lock.device_id] = self._api.get_lock_detail(
|
||||
self._access_token, lock.device_id)
|
||||
self._access_token, lock.device_id
|
||||
)
|
||||
|
||||
self._lock_status_by_id = status_by_id
|
||||
self._lock_detail_by_id = detail_by_id
|
||||
|
|
|
|||
|
|
@ -126,8 +126,11 @@ from datetime import timedelta
|
|||
from aiohttp import web
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.auth.models import User, Credentials, \
|
||||
TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN
|
||||
from homeassistant.auth.models import (
|
||||
User,
|
||||
Credentials,
|
||||
TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN,
|
||||
)
|
||||
from homeassistant.components import websocket_api
|
||||
from homeassistant.components.http import KEY_REAL_IP
|
||||
from homeassistant.components.http.ban import log_invalid_auth
|
||||
|
|
@ -140,38 +143,39 @@ from . import indieauth
|
|||
from . import login_flow
|
||||
from . import mfa_setup_flow
|
||||
|
||||
DOMAIN = 'auth'
|
||||
DEPENDENCIES = ['http']
|
||||
DOMAIN = "auth"
|
||||
DEPENDENCIES = ["http"]
|
||||
|
||||
WS_TYPE_CURRENT_USER = 'auth/current_user'
|
||||
SCHEMA_WS_CURRENT_USER = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
||||
vol.Required('type'): WS_TYPE_CURRENT_USER,
|
||||
})
|
||||
WS_TYPE_CURRENT_USER = "auth/current_user"
|
||||
SCHEMA_WS_CURRENT_USER = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
|
||||
{vol.Required("type"): WS_TYPE_CURRENT_USER}
|
||||
)
|
||||
|
||||
WS_TYPE_LONG_LIVED_ACCESS_TOKEN = 'auth/long_lived_access_token'
|
||||
SCHEMA_WS_LONG_LIVED_ACCESS_TOKEN = \
|
||||
websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
||||
vol.Required('type'): WS_TYPE_LONG_LIVED_ACCESS_TOKEN,
|
||||
vol.Required('lifespan'): int, # days
|
||||
vol.Required('client_name'): str,
|
||||
vol.Optional('client_icon'): str,
|
||||
})
|
||||
WS_TYPE_LONG_LIVED_ACCESS_TOKEN = "auth/long_lived_access_token"
|
||||
SCHEMA_WS_LONG_LIVED_ACCESS_TOKEN = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
|
||||
{
|
||||
vol.Required("type"): WS_TYPE_LONG_LIVED_ACCESS_TOKEN,
|
||||
vol.Required("lifespan"): int, # days
|
||||
vol.Required("client_name"): str,
|
||||
vol.Optional("client_icon"): str,
|
||||
}
|
||||
)
|
||||
|
||||
WS_TYPE_REFRESH_TOKENS = 'auth/refresh_tokens'
|
||||
SCHEMA_WS_REFRESH_TOKENS = \
|
||||
websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
||||
vol.Required('type'): WS_TYPE_REFRESH_TOKENS,
|
||||
})
|
||||
WS_TYPE_REFRESH_TOKENS = "auth/refresh_tokens"
|
||||
SCHEMA_WS_REFRESH_TOKENS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
|
||||
{vol.Required("type"): WS_TYPE_REFRESH_TOKENS}
|
||||
)
|
||||
|
||||
WS_TYPE_DELETE_REFRESH_TOKEN = 'auth/delete_refresh_token'
|
||||
SCHEMA_WS_DELETE_REFRESH_TOKEN = \
|
||||
websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
||||
vol.Required('type'): WS_TYPE_DELETE_REFRESH_TOKEN,
|
||||
vol.Required('refresh_token_id'): str,
|
||||
})
|
||||
WS_TYPE_DELETE_REFRESH_TOKEN = "auth/delete_refresh_token"
|
||||
SCHEMA_WS_DELETE_REFRESH_TOKEN = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
|
||||
{
|
||||
vol.Required("type"): WS_TYPE_DELETE_REFRESH_TOKEN,
|
||||
vol.Required("refresh_token_id"): str,
|
||||
}
|
||||
)
|
||||
|
||||
RESULT_TYPE_CREDENTIALS = 'credentials'
|
||||
RESULT_TYPE_USER = 'user'
|
||||
RESULT_TYPE_CREDENTIALS = "credentials"
|
||||
RESULT_TYPE_USER = "user"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -184,23 +188,20 @@ async def async_setup(hass, config):
|
|||
hass.http.register_view(LinkUserView(retrieve_result))
|
||||
|
||||
hass.components.websocket_api.async_register_command(
|
||||
WS_TYPE_CURRENT_USER, websocket_current_user,
|
||||
SCHEMA_WS_CURRENT_USER
|
||||
WS_TYPE_CURRENT_USER, websocket_current_user, SCHEMA_WS_CURRENT_USER
|
||||
)
|
||||
hass.components.websocket_api.async_register_command(
|
||||
WS_TYPE_LONG_LIVED_ACCESS_TOKEN,
|
||||
websocket_create_long_lived_access_token,
|
||||
SCHEMA_WS_LONG_LIVED_ACCESS_TOKEN
|
||||
SCHEMA_WS_LONG_LIVED_ACCESS_TOKEN,
|
||||
)
|
||||
hass.components.websocket_api.async_register_command(
|
||||
WS_TYPE_REFRESH_TOKENS,
|
||||
websocket_refresh_tokens,
|
||||
SCHEMA_WS_REFRESH_TOKENS
|
||||
WS_TYPE_REFRESH_TOKENS, websocket_refresh_tokens, SCHEMA_WS_REFRESH_TOKENS
|
||||
)
|
||||
hass.components.websocket_api.async_register_command(
|
||||
WS_TYPE_DELETE_REFRESH_TOKEN,
|
||||
websocket_delete_refresh_token,
|
||||
SCHEMA_WS_DELETE_REFRESH_TOKEN
|
||||
SCHEMA_WS_DELETE_REFRESH_TOKEN,
|
||||
)
|
||||
|
||||
await login_flow.async_setup(hass, store_result)
|
||||
|
|
@ -212,8 +213,8 @@ async def async_setup(hass, config):
|
|||
class TokenView(HomeAssistantView):
|
||||
"""View to issue or revoke tokens."""
|
||||
|
||||
url = '/auth/token'
|
||||
name = 'api:auth:token'
|
||||
url = "/auth/token"
|
||||
name = "api:auth:token"
|
||||
requires_auth = False
|
||||
cors_allowed = True
|
||||
|
||||
|
|
@ -224,29 +225,29 @@ class TokenView(HomeAssistantView):
|
|||
@log_invalid_auth
|
||||
async def post(self, request):
|
||||
"""Grant a token."""
|
||||
hass = request.app['hass']
|
||||
hass = request.app["hass"]
|
||||
data = await request.post()
|
||||
|
||||
grant_type = data.get('grant_type')
|
||||
grant_type = data.get("grant_type")
|
||||
|
||||
# IndieAuth 6.3.5
|
||||
# The revocation endpoint is the same as the token endpoint.
|
||||
# The revocation request includes an additional parameter,
|
||||
# action=revoke.
|
||||
if data.get('action') == 'revoke':
|
||||
if data.get("action") == "revoke":
|
||||
return await self._async_handle_revoke_token(hass, data)
|
||||
|
||||
if grant_type == 'authorization_code':
|
||||
if grant_type == "authorization_code":
|
||||
return await self._async_handle_auth_code(
|
||||
hass, data, str(request[KEY_REAL_IP]))
|
||||
hass, data, str(request[KEY_REAL_IP])
|
||||
)
|
||||
|
||||
if grant_type == 'refresh_token':
|
||||
if grant_type == "refresh_token":
|
||||
return await self._async_handle_refresh_token(
|
||||
hass, data, str(request[KEY_REAL_IP]))
|
||||
hass, data, str(request[KEY_REAL_IP])
|
||||
)
|
||||
|
||||
return self.json({
|
||||
'error': 'unsupported_grant_type',
|
||||
}, status_code=400)
|
||||
return self.json({"error": "unsupported_grant_type"}, status_code=400)
|
||||
|
||||
async def _async_handle_revoke_token(self, hass, data):
|
||||
"""Handle revoke token request."""
|
||||
|
|
@ -254,7 +255,7 @@ class TokenView(HomeAssistantView):
|
|||
# 2.2 The authorization server responds with HTTP status code 200
|
||||
# if the token has been revoked successfully or if the client
|
||||
# submitted an invalid token.
|
||||
token = data.get('token')
|
||||
token = data.get("token")
|
||||
|
||||
if token is None:
|
||||
return web.Response(status=200)
|
||||
|
|
@ -269,117 +270,112 @@ class TokenView(HomeAssistantView):
|
|||
|
||||
async def _async_handle_auth_code(self, hass, data, remote_addr):
|
||||
"""Handle authorization code request."""
|
||||
client_id = data.get('client_id')
|
||||
client_id = data.get("client_id")
|
||||
if client_id is None or not indieauth.verify_client_id(client_id):
|
||||
return self.json({
|
||||
'error': 'invalid_request',
|
||||
'error_description': 'Invalid client id',
|
||||
}, status_code=400)
|
||||
return self.json(
|
||||
{"error": "invalid_request", "error_description": "Invalid client id"},
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
code = data.get('code')
|
||||
code = data.get("code")
|
||||
|
||||
if code is None:
|
||||
return self.json({
|
||||
'error': 'invalid_request',
|
||||
'error_description': 'Invalid code',
|
||||
}, status_code=400)
|
||||
return self.json(
|
||||
{"error": "invalid_request", "error_description": "Invalid code"},
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
user = self._retrieve_user(client_id, RESULT_TYPE_USER, code)
|
||||
|
||||
if user is None or not isinstance(user, User):
|
||||
return self.json({
|
||||
'error': 'invalid_request',
|
||||
'error_description': 'Invalid code',
|
||||
}, status_code=400)
|
||||
return self.json(
|
||||
{"error": "invalid_request", "error_description": "Invalid code"},
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
# refresh user
|
||||
user = await hass.auth.async_get_user(user.id)
|
||||
|
||||
if not user.is_active:
|
||||
return self.json({
|
||||
'error': 'access_denied',
|
||||
'error_description': 'User is not active',
|
||||
}, status_code=403)
|
||||
return self.json(
|
||||
{"error": "access_denied", "error_description": "User is not active"},
|
||||
status_code=403,
|
||||
)
|
||||
|
||||
refresh_token = await hass.auth.async_create_refresh_token(user,
|
||||
client_id)
|
||||
access_token = hass.auth.async_create_access_token(
|
||||
refresh_token, remote_addr)
|
||||
refresh_token = await hass.auth.async_create_refresh_token(user, client_id)
|
||||
access_token = hass.auth.async_create_access_token(refresh_token, remote_addr)
|
||||
|
||||
return self.json({
|
||||
'access_token': access_token,
|
||||
'token_type': 'Bearer',
|
||||
'refresh_token': refresh_token.token,
|
||||
'expires_in':
|
||||
int(refresh_token.access_token_expiration.total_seconds()),
|
||||
})
|
||||
return self.json(
|
||||
{
|
||||
"access_token": access_token,
|
||||
"token_type": "Bearer",
|
||||
"refresh_token": refresh_token.token,
|
||||
"expires_in": int(
|
||||
refresh_token.access_token_expiration.total_seconds()
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
async def _async_handle_refresh_token(self, hass, data, remote_addr):
|
||||
"""Handle authorization code request."""
|
||||
client_id = data.get('client_id')
|
||||
client_id = data.get("client_id")
|
||||
if client_id is not None and not indieauth.verify_client_id(client_id):
|
||||
return self.json({
|
||||
'error': 'invalid_request',
|
||||
'error_description': 'Invalid client id',
|
||||
}, status_code=400)
|
||||
return self.json(
|
||||
{"error": "invalid_request", "error_description": "Invalid client id"},
|
||||
status_code=400,
|
||||
)
|
||||
|
||||
token = data.get('refresh_token')
|
||||
token = data.get("refresh_token")
|
||||
|
||||
if token is None:
|
||||
return self.json({
|
||||
'error': 'invalid_request',
|
||||
}, status_code=400)
|
||||
return self.json({"error": "invalid_request"}, status_code=400)
|
||||
|
||||
refresh_token = await hass.auth.async_get_refresh_token_by_token(token)
|
||||
|
||||
if refresh_token is None:
|
||||
return self.json({
|
||||
'error': 'invalid_grant',
|
||||
}, status_code=400)
|
||||
return self.json({"error": "invalid_grant"}, status_code=400)
|
||||
|
||||
if refresh_token.client_id != client_id:
|
||||
return self.json({
|
||||
'error': 'invalid_request',
|
||||
}, status_code=400)
|
||||
return self.json({"error": "invalid_request"}, status_code=400)
|
||||
|
||||
access_token = hass.auth.async_create_access_token(
|
||||
refresh_token, remote_addr)
|
||||
access_token = hass.auth.async_create_access_token(refresh_token, remote_addr)
|
||||
|
||||
return self.json({
|
||||
'access_token': access_token,
|
||||
'token_type': 'Bearer',
|
||||
'expires_in':
|
||||
int(refresh_token.access_token_expiration.total_seconds()),
|
||||
})
|
||||
return self.json(
|
||||
{
|
||||
"access_token": access_token,
|
||||
"token_type": "Bearer",
|
||||
"expires_in": int(
|
||||
refresh_token.access_token_expiration.total_seconds()
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class LinkUserView(HomeAssistantView):
|
||||
"""View to link existing users to new credentials."""
|
||||
|
||||
url = '/auth/link_user'
|
||||
name = 'api:auth:link_user'
|
||||
url = "/auth/link_user"
|
||||
name = "api:auth:link_user"
|
||||
|
||||
def __init__(self, retrieve_credentials):
|
||||
"""Initialize the link user view."""
|
||||
self._retrieve_credentials = retrieve_credentials
|
||||
|
||||
@RequestDataValidator(vol.Schema({
|
||||
'code': str,
|
||||
'client_id': str,
|
||||
}))
|
||||
@RequestDataValidator(vol.Schema({"code": str, "client_id": str}))
|
||||
async def post(self, request, data):
|
||||
"""Link a user."""
|
||||
hass = request.app['hass']
|
||||
user = request['hass_user']
|
||||
hass = request.app["hass"]
|
||||
user = request["hass_user"]
|
||||
|
||||
credentials = self._retrieve_credentials(
|
||||
data['client_id'], RESULT_TYPE_CREDENTIALS, data['code'])
|
||||
data["client_id"], RESULT_TYPE_CREDENTIALS, data["code"]
|
||||
)
|
||||
|
||||
if credentials is None:
|
||||
return self.json_message('Invalid code', status_code=400)
|
||||
return self.json_message("Invalid code", status_code=400)
|
||||
|
||||
await hass.auth.async_link_user(user, credentials)
|
||||
return self.json_message('User linked')
|
||||
return self.json_message("User linked")
|
||||
|
||||
|
||||
@callback
|
||||
|
|
@ -395,11 +391,14 @@ def _create_auth_code_store():
|
|||
elif isinstance(result, Credentials):
|
||||
result_type = RESULT_TYPE_CREDENTIALS
|
||||
else:
|
||||
raise ValueError('result has to be either User or Credentials')
|
||||
raise ValueError("result has to be either User or Credentials")
|
||||
|
||||
code = uuid.uuid4().hex
|
||||
temp_results[(client_id, result_type, code)] = \
|
||||
(dt_util.utcnow(), result_type, result)
|
||||
temp_results[(client_id, result_type, code)] = (
|
||||
dt_util.utcnow(),
|
||||
result_type,
|
||||
result,
|
||||
)
|
||||
return code
|
||||
|
||||
@callback
|
||||
|
|
@ -427,26 +426,39 @@ def _create_auth_code_store():
|
|||
@websocket_api.ws_require_user()
|
||||
@callback
|
||||
def websocket_current_user(
|
||||
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg):
|
||||
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg
|
||||
):
|
||||
"""Return the current user."""
|
||||
|
||||
async def async_get_current_user(user):
|
||||
"""Get current user."""
|
||||
enabled_modules = await hass.auth.async_get_enabled_mfa(user)
|
||||
|
||||
connection.send_message_outside(
|
||||
websocket_api.result_message(msg['id'], {
|
||||
'id': user.id,
|
||||
'name': user.name,
|
||||
'is_owner': user.is_owner,
|
||||
'credentials': [{'auth_provider_type': c.auth_provider_type,
|
||||
'auth_provider_id': c.auth_provider_id}
|
||||
for c in user.credentials],
|
||||
'mfa_modules': [{
|
||||
'id': module.id,
|
||||
'name': module.name,
|
||||
'enabled': module.id in enabled_modules,
|
||||
} for module in hass.auth.auth_mfa_modules],
|
||||
}))
|
||||
websocket_api.result_message(
|
||||
msg["id"],
|
||||
{
|
||||
"id": user.id,
|
||||
"name": user.name,
|
||||
"is_owner": user.is_owner,
|
||||
"credentials": [
|
||||
{
|
||||
"auth_provider_type": c.auth_provider_type,
|
||||
"auth_provider_id": c.auth_provider_id,
|
||||
}
|
||||
for c in user.credentials
|
||||
],
|
||||
"mfa_modules": [
|
||||
{
|
||||
"id": module.id,
|
||||
"name": module.name,
|
||||
"enabled": module.id in enabled_modules,
|
||||
}
|
||||
for module in hass.auth.auth_mfa_modules
|
||||
],
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
hass.async_create_task(async_get_current_user(connection.user))
|
||||
|
||||
|
|
@ -454,63 +466,77 @@ def websocket_current_user(
|
|||
@websocket_api.ws_require_user()
|
||||
@callback
|
||||
def websocket_create_long_lived_access_token(
|
||||
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg):
|
||||
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg
|
||||
):
|
||||
"""Create or a long-lived access token."""
|
||||
|
||||
async def async_create_long_lived_access_token(user):
|
||||
"""Create or a long-lived access token."""
|
||||
refresh_token = await hass.auth.async_create_refresh_token(
|
||||
user,
|
||||
client_name=msg['client_name'],
|
||||
client_icon=msg.get('client_icon'),
|
||||
client_name=msg["client_name"],
|
||||
client_icon=msg.get("client_icon"),
|
||||
token_type=TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN,
|
||||
access_token_expiration=timedelta(days=msg['lifespan']))
|
||||
access_token_expiration=timedelta(days=msg["lifespan"]),
|
||||
)
|
||||
|
||||
access_token = hass.auth.async_create_access_token(
|
||||
refresh_token)
|
||||
access_token = hass.auth.async_create_access_token(refresh_token)
|
||||
|
||||
connection.send_message_outside(
|
||||
websocket_api.result_message(msg['id'], access_token))
|
||||
websocket_api.result_message(msg["id"], access_token)
|
||||
)
|
||||
|
||||
hass.async_create_task(
|
||||
async_create_long_lived_access_token(connection.user))
|
||||
hass.async_create_task(async_create_long_lived_access_token(connection.user))
|
||||
|
||||
|
||||
@websocket_api.ws_require_user()
|
||||
@callback
|
||||
def websocket_refresh_tokens(
|
||||
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg):
|
||||
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg
|
||||
):
|
||||
"""Return metadata of users refresh tokens."""
|
||||
current_id = connection.request.get('refresh_token_id')
|
||||
connection.to_write.put_nowait(websocket_api.result_message(msg['id'], [{
|
||||
'id': refresh.id,
|
||||
'client_id': refresh.client_id,
|
||||
'client_name': refresh.client_name,
|
||||
'client_icon': refresh.client_icon,
|
||||
'type': refresh.token_type,
|
||||
'created_at': refresh.created_at,
|
||||
'is_current': refresh.id == current_id,
|
||||
'last_used_at': refresh.last_used_at,
|
||||
'last_used_ip': refresh.last_used_ip,
|
||||
} for refresh in connection.user.refresh_tokens.values()]))
|
||||
current_id = connection.request.get("refresh_token_id")
|
||||
connection.to_write.put_nowait(
|
||||
websocket_api.result_message(
|
||||
msg["id"],
|
||||
[
|
||||
{
|
||||
"id": refresh.id,
|
||||
"client_id": refresh.client_id,
|
||||
"client_name": refresh.client_name,
|
||||
"client_icon": refresh.client_icon,
|
||||
"type": refresh.token_type,
|
||||
"created_at": refresh.created_at,
|
||||
"is_current": refresh.id == current_id,
|
||||
"last_used_at": refresh.last_used_at,
|
||||
"last_used_ip": refresh.last_used_ip,
|
||||
}
|
||||
for refresh in connection.user.refresh_tokens.values()
|
||||
],
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@websocket_api.ws_require_user()
|
||||
@callback
|
||||
def websocket_delete_refresh_token(
|
||||
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg):
|
||||
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg
|
||||
):
|
||||
"""Handle a delete refresh token request."""
|
||||
|
||||
async def async_delete_refresh_token(user, refresh_token_id):
|
||||
"""Delete a refresh token."""
|
||||
refresh_token = connection.user.refresh_tokens.get(refresh_token_id)
|
||||
|
||||
if refresh_token is None:
|
||||
return websocket_api.error_message(
|
||||
msg['id'], 'invalid_token_id', 'Received invalid token')
|
||||
msg["id"], "invalid_token_id", "Received invalid token"
|
||||
)
|
||||
|
||||
await hass.auth.async_remove_refresh_token(refresh_token)
|
||||
|
||||
connection.send_message_outside(
|
||||
websocket_api.result_message(msg['id'], {}))
|
||||
connection.send_message_outside(websocket_api.result_message(msg["id"], {}))
|
||||
|
||||
hass.async_create_task(
|
||||
async_delete_refresh_token(connection.user, msg['refresh_token_id']))
|
||||
async_delete_refresh_token(connection.user, msg["refresh_token_id"])
|
||||
)
|
||||
|
|
|
|||
|
|
@ -8,16 +8,13 @@ import aiohttp
|
|||
from aiohttp.client_exceptions import ClientError
|
||||
|
||||
# IP addresses of loopback interfaces
|
||||
ALLOWED_IPS = (
|
||||
ip_address('127.0.0.1'),
|
||||
ip_address('::1'),
|
||||
)
|
||||
ALLOWED_IPS = (ip_address("127.0.0.1"), ip_address("::1"))
|
||||
|
||||
# RFC1918 - Address allocation for Private Internets
|
||||
ALLOWED_NETWORKS = (
|
||||
ip_network('10.0.0.0/8'),
|
||||
ip_network('172.16.0.0/12'),
|
||||
ip_network('192.168.0.0/16'),
|
||||
ip_network("10.0.0.0/8"),
|
||||
ip_network("172.16.0.0/12"),
|
||||
ip_network("192.168.0.0/16"),
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -32,8 +29,8 @@ async def verify_redirect_uri(hass, client_id, redirect_uri):
|
|||
|
||||
# Verify redirect url and client url have same scheme and domain.
|
||||
is_valid = (
|
||||
client_id_parts.scheme == redirect_parts.scheme and
|
||||
client_id_parts.netloc == redirect_parts.netloc
|
||||
client_id_parts.scheme == redirect_parts.scheme
|
||||
and client_id_parts.netloc == redirect_parts.netloc
|
||||
)
|
||||
|
||||
if is_valid:
|
||||
|
|
@ -56,13 +53,13 @@ class LinkTagParser(HTMLParser):
|
|||
|
||||
def handle_starttag(self, tag, attrs):
|
||||
"""Handle finding a start tag."""
|
||||
if tag != 'link':
|
||||
if tag != "link":
|
||||
return
|
||||
|
||||
attrs = dict(attrs)
|
||||
|
||||
if attrs.get('rel') == self.rel:
|
||||
self.found.append(attrs.get('href'))
|
||||
if attrs.get("rel") == self.rel:
|
||||
self.found.append(attrs.get("href"))
|
||||
|
||||
|
||||
async def fetch_redirect_uris(hass, url):
|
||||
|
|
@ -77,7 +74,7 @@ async def fetch_redirect_uris(hass, url):
|
|||
|
||||
We do not implement extracting redirect uris from headers.
|
||||
"""
|
||||
parser = LinkTagParser('redirect_uri')
|
||||
parser = LinkTagParser("redirect_uri")
|
||||
chunks = 0
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
|
|
@ -119,8 +116,8 @@ def _parse_url(url):
|
|||
|
||||
# If a URL with no path component is ever encountered,
|
||||
# it MUST be treated as if it had the path /.
|
||||
if parts.path == '':
|
||||
parts = parts._replace(path='/')
|
||||
if parts.path == "":
|
||||
parts = parts._replace(path="/")
|
||||
|
||||
return parts
|
||||
|
||||
|
|
@ -134,34 +131,35 @@ def _parse_client_id(client_id):
|
|||
|
||||
# Client identifier URLs
|
||||
# MUST have either an https or http scheme
|
||||
if parts.scheme not in ('http', 'https'):
|
||||
if parts.scheme not in ("http", "https"):
|
||||
raise ValueError()
|
||||
|
||||
# MUST contain a path component
|
||||
# Handled by url canonicalization.
|
||||
|
||||
# MUST NOT contain single-dot or double-dot path segments
|
||||
if any(segment in ('.', '..') for segment in parts.path.split('/')):
|
||||
if any(segment in (".", "..") for segment in parts.path.split("/")):
|
||||
raise ValueError(
|
||||
'Client ID cannot contain single-dot or double-dot path segments')
|
||||
"Client ID cannot contain single-dot or double-dot path segments"
|
||||
)
|
||||
|
||||
# MUST NOT contain a fragment component
|
||||
if parts.fragment != '':
|
||||
raise ValueError('Client ID cannot contain a fragment')
|
||||
if parts.fragment != "":
|
||||
raise ValueError("Client ID cannot contain a fragment")
|
||||
|
||||
# MUST NOT contain a username or password component
|
||||
if parts.username is not None:
|
||||
raise ValueError('Client ID cannot contain username')
|
||||
raise ValueError("Client ID cannot contain username")
|
||||
|
||||
if parts.password is not None:
|
||||
raise ValueError('Client ID cannot contain password')
|
||||
raise ValueError("Client ID cannot contain password")
|
||||
|
||||
# MAY contain a port
|
||||
try:
|
||||
# parts raises ValueError when port cannot be parsed as int
|
||||
parts.port
|
||||
except ValueError:
|
||||
raise ValueError('Client ID contains invalid port')
|
||||
raise ValueError("Client ID contains invalid port")
|
||||
|
||||
# Additionally, hostnames
|
||||
# MUST be domain names or a loopback interface and
|
||||
|
|
@ -177,7 +175,7 @@ def _parse_client_id(client_id):
|
|||
netloc = parts.netloc
|
||||
|
||||
# Strip the [, ] from ipv6 addresses before parsing
|
||||
if netloc[0] == '[' and netloc[-1] == ']':
|
||||
if netloc[0] == "[" and netloc[-1] == "]":
|
||||
netloc = netloc[1:-1]
|
||||
|
||||
address = ip_address(netloc)
|
||||
|
|
@ -185,9 +183,11 @@ def _parse_client_id(client_id):
|
|||
# Not an ip address
|
||||
pass
|
||||
|
||||
if (address is None or
|
||||
address in ALLOWED_IPS or
|
||||
any(address in network for network in ALLOWED_NETWORKS)):
|
||||
if (
|
||||
address is None
|
||||
or address in ALLOWED_IPS
|
||||
or any(address in network for network in ALLOWED_NETWORKS)
|
||||
):
|
||||
return parts
|
||||
|
||||
raise ValueError('Hostname should be a domain name or local IP address')
|
||||
raise ValueError("Hostname should be a domain name or local IP address")
|
||||
|
|
|
|||
|
|
@ -71,8 +71,7 @@ import voluptuous as vol
|
|||
|
||||
from homeassistant import data_entry_flow
|
||||
from homeassistant.components.http import KEY_REAL_IP
|
||||
from homeassistant.components.http.ban import process_wrong_login, \
|
||||
log_invalid_auth
|
||||
from homeassistant.components.http.ban import process_wrong_login, log_invalid_auth
|
||||
from homeassistant.components.http.data_validator import RequestDataValidator
|
||||
from homeassistant.components.http.view import HomeAssistantView
|
||||
from . import indieauth
|
||||
|
|
@ -82,55 +81,55 @@ async def async_setup(hass, store_result):
|
|||
"""Component to allow users to login."""
|
||||
hass.http.register_view(AuthProvidersView)
|
||||
hass.http.register_view(LoginFlowIndexView(hass.auth.login_flow))
|
||||
hass.http.register_view(
|
||||
LoginFlowResourceView(hass.auth.login_flow, store_result))
|
||||
hass.http.register_view(LoginFlowResourceView(hass.auth.login_flow, store_result))
|
||||
|
||||
|
||||
class AuthProvidersView(HomeAssistantView):
|
||||
"""View to get available auth providers."""
|
||||
|
||||
url = '/auth/providers'
|
||||
name = 'api:auth:providers'
|
||||
url = "/auth/providers"
|
||||
name = "api:auth:providers"
|
||||
requires_auth = False
|
||||
|
||||
async def get(self, request):
|
||||
"""Get available auth providers."""
|
||||
hass = request.app['hass']
|
||||
hass = request.app["hass"]
|
||||
|
||||
if not hass.components.onboarding.async_is_onboarded():
|
||||
return self.json_message(
|
||||
message='Onboarding not finished',
|
||||
message="Onboarding not finished",
|
||||
status_code=400,
|
||||
message_code='onboarding_required'
|
||||
message_code="onboarding_required",
|
||||
)
|
||||
|
||||
return self.json([{
|
||||
'name': provider.name,
|
||||
'id': provider.id,
|
||||
'type': provider.type,
|
||||
} for provider in hass.auth.auth_providers])
|
||||
return self.json(
|
||||
[
|
||||
{"name": provider.name, "id": provider.id, "type": provider.type}
|
||||
for provider in hass.auth.auth_providers
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def _prepare_result_json(result):
|
||||
"""Convert result to JSON."""
|
||||
if result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
|
||||
if result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
|
||||
data = result.copy()
|
||||
data.pop('result')
|
||||
data.pop('data')
|
||||
data.pop("result")
|
||||
data.pop("data")
|
||||
return data
|
||||
|
||||
if result['type'] != data_entry_flow.RESULT_TYPE_FORM:
|
||||
if result["type"] != data_entry_flow.RESULT_TYPE_FORM:
|
||||
return result
|
||||
|
||||
import voluptuous_serialize
|
||||
|
||||
data = result.copy()
|
||||
|
||||
schema = data['data_schema']
|
||||
schema = data["data_schema"]
|
||||
if schema is None:
|
||||
data['data_schema'] = []
|
||||
data["data_schema"] = []
|
||||
else:
|
||||
data['data_schema'] = voluptuous_serialize.convert(schema)
|
||||
data["data_schema"] = voluptuous_serialize.convert(schema)
|
||||
|
||||
return data
|
||||
|
||||
|
|
@ -138,8 +137,8 @@ def _prepare_result_json(result):
|
|||
class LoginFlowIndexView(HomeAssistantView):
|
||||
"""View to create a config flow."""
|
||||
|
||||
url = '/auth/login_flow'
|
||||
name = 'api:auth:login_flow'
|
||||
url = "/auth/login_flow"
|
||||
name = "api:auth:login_flow"
|
||||
requires_auth = False
|
||||
|
||||
def __init__(self, flow_mgr):
|
||||
|
|
@ -150,34 +149,41 @@ class LoginFlowIndexView(HomeAssistantView):
|
|||
"""Do not allow index of flows in progress."""
|
||||
return web.Response(status=405)
|
||||
|
||||
@RequestDataValidator(vol.Schema({
|
||||
vol.Required('client_id'): str,
|
||||
vol.Required('handler'): vol.Any(str, list),
|
||||
vol.Required('redirect_uri'): str,
|
||||
vol.Optional('type', default='authorize'): str,
|
||||
}))
|
||||
@RequestDataValidator(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Required("client_id"): str,
|
||||
vol.Required("handler"): vol.Any(str, list),
|
||||
vol.Required("redirect_uri"): str,
|
||||
vol.Optional("type", default="authorize"): str,
|
||||
}
|
||||
)
|
||||
)
|
||||
@log_invalid_auth
|
||||
async def post(self, request, data):
|
||||
"""Create a new login flow."""
|
||||
if not await indieauth.verify_redirect_uri(
|
||||
request.app['hass'], data['client_id'], data['redirect_uri']):
|
||||
return self.json_message('invalid client id or redirect uri', 400)
|
||||
request.app["hass"], data["client_id"], data["redirect_uri"]
|
||||
):
|
||||
return self.json_message("invalid client id or redirect uri", 400)
|
||||
|
||||
if isinstance(data['handler'], list):
|
||||
handler = tuple(data['handler'])
|
||||
if isinstance(data["handler"], list):
|
||||
handler = tuple(data["handler"])
|
||||
else:
|
||||
handler = data['handler']
|
||||
handler = data["handler"]
|
||||
|
||||
try:
|
||||
result = await self._flow_mgr.async_init(
|
||||
handler, context={
|
||||
'ip_address': request[KEY_REAL_IP],
|
||||
'credential_only': data.get('type') == 'link_user',
|
||||
})
|
||||
handler,
|
||||
context={
|
||||
"ip_address": request[KEY_REAL_IP],
|
||||
"credential_only": data.get("type") == "link_user",
|
||||
},
|
||||
)
|
||||
except data_entry_flow.UnknownHandler:
|
||||
return self.json_message('Invalid handler specified', 404)
|
||||
return self.json_message("Invalid handler specified", 404)
|
||||
except data_entry_flow.UnknownStep:
|
||||
return self.json_message('Handler does not support init', 400)
|
||||
return self.json_message("Handler does not support init", 400)
|
||||
|
||||
return self.json(_prepare_result_json(result))
|
||||
|
||||
|
|
@ -185,8 +191,8 @@ class LoginFlowIndexView(HomeAssistantView):
|
|||
class LoginFlowResourceView(HomeAssistantView):
|
||||
"""View to interact with the flow manager."""
|
||||
|
||||
url = '/auth/login_flow/{flow_id}'
|
||||
name = 'api:auth:login_flow:resource'
|
||||
url = "/auth/login_flow/{flow_id}"
|
||||
name = "api:auth:login_flow:resource"
|
||||
requires_auth = False
|
||||
|
||||
def __init__(self, flow_mgr, store_result):
|
||||
|
|
@ -196,43 +202,43 @@ class LoginFlowResourceView(HomeAssistantView):
|
|||
|
||||
async def get(self, request):
|
||||
"""Do not allow getting status of a flow in progress."""
|
||||
return self.json_message('Invalid flow specified', 404)
|
||||
return self.json_message("Invalid flow specified", 404)
|
||||
|
||||
@RequestDataValidator(vol.Schema({
|
||||
'client_id': str
|
||||
}, extra=vol.ALLOW_EXTRA))
|
||||
@RequestDataValidator(vol.Schema({"client_id": str}, extra=vol.ALLOW_EXTRA))
|
||||
@log_invalid_auth
|
||||
async def post(self, request, flow_id, data):
|
||||
"""Handle progressing a login flow request."""
|
||||
client_id = data.pop('client_id')
|
||||
client_id = data.pop("client_id")
|
||||
|
||||
if not indieauth.verify_client_id(client_id):
|
||||
return self.json_message('Invalid client id', 400)
|
||||
return self.json_message("Invalid client id", 400)
|
||||
|
||||
try:
|
||||
# do not allow change ip during login flow
|
||||
for flow in self._flow_mgr.async_progress():
|
||||
if (flow['flow_id'] == flow_id and
|
||||
flow['context']['ip_address'] !=
|
||||
request.get(KEY_REAL_IP)):
|
||||
return self.json_message('IP address changed', 400)
|
||||
if flow["flow_id"] == flow_id and flow["context"][
|
||||
"ip_address"
|
||||
] != request.get(KEY_REAL_IP):
|
||||
return self.json_message("IP address changed", 400)
|
||||
|
||||
result = await self._flow_mgr.async_configure(flow_id, data)
|
||||
except data_entry_flow.UnknownFlow:
|
||||
return self.json_message('Invalid flow specified', 404)
|
||||
return self.json_message("Invalid flow specified", 404)
|
||||
except vol.Invalid:
|
||||
return self.json_message('User input malformed', 400)
|
||||
return self.json_message("User input malformed", 400)
|
||||
|
||||
if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
|
||||
if result["type"] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
|
||||
# @log_invalid_auth does not work here since it returns HTTP 200
|
||||
# need manually log failed login attempts
|
||||
if result['errors'] is not None and \
|
||||
result['errors'].get('base') == 'invalid_auth':
|
||||
if (
|
||||
result["errors"] is not None
|
||||
and result["errors"].get("base") == "invalid_auth"
|
||||
):
|
||||
await process_wrong_login(request)
|
||||
return self.json(_prepare_result_json(result))
|
||||
|
||||
result.pop('data')
|
||||
result['result'] = self._store_result(client_id, result['result'])
|
||||
result.pop("data")
|
||||
result["result"] = self._store_result(client_id, result["result"])
|
||||
|
||||
return self.json(result)
|
||||
|
||||
|
|
@ -241,6 +247,6 @@ class LoginFlowResourceView(HomeAssistantView):
|
|||
try:
|
||||
self._flow_mgr.async_abort(flow_id)
|
||||
except data_entry_flow.UnknownFlow:
|
||||
return self.json_message('Invalid flow specified', 404)
|
||||
return self.json_message("Invalid flow specified", 404)
|
||||
|
||||
return self.json_message('Flow aborted')
|
||||
return self.json_message("Flow aborted")
|
||||
|
|
|
|||
|
|
@ -7,82 +7,93 @@ from homeassistant import data_entry_flow
|
|||
from homeassistant.components import websocket_api
|
||||
from homeassistant.core import callback, HomeAssistant
|
||||
|
||||
WS_TYPE_SETUP_MFA = 'auth/setup_mfa'
|
||||
SCHEMA_WS_SETUP_MFA = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
||||
vol.Required('type'): WS_TYPE_SETUP_MFA,
|
||||
vol.Exclusive('mfa_module_id', 'module_or_flow_id'): str,
|
||||
vol.Exclusive('flow_id', 'module_or_flow_id'): str,
|
||||
vol.Optional('user_input'): object,
|
||||
})
|
||||
WS_TYPE_SETUP_MFA = "auth/setup_mfa"
|
||||
SCHEMA_WS_SETUP_MFA = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
|
||||
{
|
||||
vol.Required("type"): WS_TYPE_SETUP_MFA,
|
||||
vol.Exclusive("mfa_module_id", "module_or_flow_id"): str,
|
||||
vol.Exclusive("flow_id", "module_or_flow_id"): str,
|
||||
vol.Optional("user_input"): object,
|
||||
}
|
||||
)
|
||||
|
||||
WS_TYPE_DEPOSE_MFA = 'auth/depose_mfa'
|
||||
SCHEMA_WS_DEPOSE_MFA = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
||||
vol.Required('type'): WS_TYPE_DEPOSE_MFA,
|
||||
vol.Required('mfa_module_id'): str,
|
||||
})
|
||||
WS_TYPE_DEPOSE_MFA = "auth/depose_mfa"
|
||||
SCHEMA_WS_DEPOSE_MFA = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
|
||||
{vol.Required("type"): WS_TYPE_DEPOSE_MFA, vol.Required("mfa_module_id"): str}
|
||||
)
|
||||
|
||||
DATA_SETUP_FLOW_MGR = 'auth_mfa_setup_flow_manager'
|
||||
DATA_SETUP_FLOW_MGR = "auth_mfa_setup_flow_manager"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup(hass):
|
||||
"""Init mfa setup flow manager."""
|
||||
|
||||
async def _async_create_setup_flow(handler, context, data):
|
||||
"""Create a setup flow. hanlder is a mfa module."""
|
||||
mfa_module = hass.auth.get_auth_mfa_module(handler)
|
||||
if mfa_module is None:
|
||||
raise ValueError('Mfa module {} is not found'.format(handler))
|
||||
raise ValueError("Mfa module {} is not found".format(handler))
|
||||
|
||||
user_id = data.pop('user_id')
|
||||
user_id = data.pop("user_id")
|
||||
return await mfa_module.async_setup_flow(user_id)
|
||||
|
||||
async def _async_finish_setup_flow(flow, flow_result):
|
||||
_LOGGER.debug('flow_result: %s', flow_result)
|
||||
_LOGGER.debug("flow_result: %s", flow_result)
|
||||
return flow_result
|
||||
|
||||
hass.data[DATA_SETUP_FLOW_MGR] = data_entry_flow.FlowManager(
|
||||
hass, _async_create_setup_flow, _async_finish_setup_flow)
|
||||
hass, _async_create_setup_flow, _async_finish_setup_flow
|
||||
)
|
||||
|
||||
hass.components.websocket_api.async_register_command(
|
||||
WS_TYPE_SETUP_MFA, websocket_setup_mfa, SCHEMA_WS_SETUP_MFA)
|
||||
WS_TYPE_SETUP_MFA, websocket_setup_mfa, SCHEMA_WS_SETUP_MFA
|
||||
)
|
||||
|
||||
hass.components.websocket_api.async_register_command(
|
||||
WS_TYPE_DEPOSE_MFA, websocket_depose_mfa, SCHEMA_WS_DEPOSE_MFA)
|
||||
WS_TYPE_DEPOSE_MFA, websocket_depose_mfa, SCHEMA_WS_DEPOSE_MFA
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
@websocket_api.ws_require_user(allow_system_user=False)
|
||||
def websocket_setup_mfa(
|
||||
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg):
|
||||
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg
|
||||
):
|
||||
"""Return a setup flow for mfa auth module."""
|
||||
|
||||
async def async_setup_flow(msg):
|
||||
"""Return a setup flow for mfa auth module."""
|
||||
flow_manager = hass.data[DATA_SETUP_FLOW_MGR]
|
||||
|
||||
flow_id = msg.get('flow_id')
|
||||
flow_id = msg.get("flow_id")
|
||||
if flow_id is not None:
|
||||
result = await flow_manager.async_configure(
|
||||
flow_id, msg.get('user_input'))
|
||||
result = await flow_manager.async_configure(flow_id, msg.get("user_input"))
|
||||
connection.send_message_outside(
|
||||
websocket_api.result_message(
|
||||
msg['id'], _prepare_result_json(result)))
|
||||
websocket_api.result_message(msg["id"], _prepare_result_json(result))
|
||||
)
|
||||
return
|
||||
|
||||
mfa_module_id = msg.get('mfa_module_id')
|
||||
mfa_module_id = msg.get("mfa_module_id")
|
||||
mfa_module = hass.auth.get_auth_mfa_module(mfa_module_id)
|
||||
if mfa_module is None:
|
||||
connection.send_message_outside(websocket_api.error_message(
|
||||
msg['id'], 'no_module',
|
||||
'MFA module {} is not found'.format(mfa_module_id)))
|
||||
connection.send_message_outside(
|
||||
websocket_api.error_message(
|
||||
msg["id"],
|
||||
"no_module",
|
||||
"MFA module {} is not found".format(mfa_module_id),
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
result = await flow_manager.async_init(
|
||||
mfa_module_id, data={'user_id': connection.user.id})
|
||||
mfa_module_id, data={"user_id": connection.user.id}
|
||||
)
|
||||
|
||||
connection.send_message_outside(
|
||||
websocket_api.result_message(
|
||||
msg['id'], _prepare_result_json(result)))
|
||||
websocket_api.result_message(msg["id"], _prepare_result_json(result))
|
||||
)
|
||||
|
||||
hass.async_create_task(async_setup_flow(msg))
|
||||
|
||||
|
|
@ -90,45 +101,49 @@ def websocket_setup_mfa(
|
|||
@callback
|
||||
@websocket_api.ws_require_user(allow_system_user=False)
|
||||
def websocket_depose_mfa(
|
||||
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg):
|
||||
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg
|
||||
):
|
||||
"""Remove user from mfa module."""
|
||||
|
||||
async def async_depose(msg):
|
||||
"""Remove user from mfa auth module."""
|
||||
mfa_module_id = msg['mfa_module_id']
|
||||
mfa_module_id = msg["mfa_module_id"]
|
||||
try:
|
||||
await hass.auth.async_disable_user_mfa(
|
||||
connection.user, msg['mfa_module_id'])
|
||||
connection.user, msg["mfa_module_id"]
|
||||
)
|
||||
except ValueError as err:
|
||||
connection.send_message_outside(websocket_api.error_message(
|
||||
msg['id'], 'disable_failed',
|
||||
'Cannot disable MFA Module {}: {}'.format(
|
||||
mfa_module_id, err)))
|
||||
connection.send_message_outside(
|
||||
websocket_api.error_message(
|
||||
msg["id"],
|
||||
"disable_failed",
|
||||
"Cannot disable MFA Module {}: {}".format(mfa_module_id, err),
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
connection.send_message_outside(
|
||||
websocket_api.result_message(
|
||||
msg['id'], 'done'))
|
||||
connection.send_message_outside(websocket_api.result_message(msg["id"], "done"))
|
||||
|
||||
hass.async_create_task(async_depose(msg))
|
||||
|
||||
|
||||
def _prepare_result_json(result):
|
||||
"""Convert result to JSON."""
|
||||
if result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
|
||||
if result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
|
||||
data = result.copy()
|
||||
return data
|
||||
|
||||
if result['type'] != data_entry_flow.RESULT_TYPE_FORM:
|
||||
if result["type"] != data_entry_flow.RESULT_TYPE_FORM:
|
||||
return result
|
||||
|
||||
import voluptuous_serialize
|
||||
|
||||
data = result.copy()
|
||||
|
||||
schema = data['data_schema']
|
||||
schema = data["data_schema"]
|
||||
if schema is None:
|
||||
data['data_schema'] = []
|
||||
data["data_schema"] = []
|
||||
else:
|
||||
data['data_schema'] = voluptuous_serialize.convert(schema)
|
||||
data["data_schema"] = voluptuous_serialize.convert(schema)
|
||||
|
||||
return data
|
||||
|
|
|
|||
|
|
@ -15,8 +15,16 @@ from homeassistant.setup import async_prepare_setup_platform
|
|||
from homeassistant.core import CoreState
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, CONF_PLATFORM, STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF,
|
||||
SERVICE_TOGGLE, SERVICE_RELOAD, EVENT_HOMEASSISTANT_START, CONF_ID)
|
||||
ATTR_ENTITY_ID,
|
||||
CONF_PLATFORM,
|
||||
STATE_ON,
|
||||
SERVICE_TURN_ON,
|
||||
SERVICE_TURN_OFF,
|
||||
SERVICE_TOGGLE,
|
||||
SERVICE_RELOAD,
|
||||
EVENT_HOMEASSISTANT_START,
|
||||
CONF_ID,
|
||||
)
|
||||
from homeassistant.components import logbook
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import extract_domain_configs, script, condition
|
||||
|
|
@ -26,32 +34,32 @@ from homeassistant.helpers.restore_state import async_get_last_state
|
|||
from homeassistant.util.dt import utcnow
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
DOMAIN = 'automation'
|
||||
DEPENDENCIES = ['group']
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
DOMAIN = "automation"
|
||||
DEPENDENCIES = ["group"]
|
||||
ENTITY_ID_FORMAT = DOMAIN + ".{}"
|
||||
|
||||
GROUP_NAME_ALL_AUTOMATIONS = 'all automations'
|
||||
GROUP_NAME_ALL_AUTOMATIONS = "all automations"
|
||||
|
||||
CONF_ALIAS = 'alias'
|
||||
CONF_HIDE_ENTITY = 'hide_entity'
|
||||
CONF_ALIAS = "alias"
|
||||
CONF_HIDE_ENTITY = "hide_entity"
|
||||
|
||||
CONF_CONDITION = 'condition'
|
||||
CONF_ACTION = 'action'
|
||||
CONF_TRIGGER = 'trigger'
|
||||
CONF_CONDITION_TYPE = 'condition_type'
|
||||
CONF_INITIAL_STATE = 'initial_state'
|
||||
CONF_CONDITION = "condition"
|
||||
CONF_ACTION = "action"
|
||||
CONF_TRIGGER = "trigger"
|
||||
CONF_CONDITION_TYPE = "condition_type"
|
||||
CONF_INITIAL_STATE = "initial_state"
|
||||
|
||||
CONDITION_USE_TRIGGER_VALUES = 'use_trigger_values'
|
||||
CONDITION_TYPE_AND = 'and'
|
||||
CONDITION_TYPE_OR = 'or'
|
||||
CONDITION_USE_TRIGGER_VALUES = "use_trigger_values"
|
||||
CONDITION_TYPE_AND = "and"
|
||||
CONDITION_TYPE_OR = "or"
|
||||
|
||||
DEFAULT_CONDITION_TYPE = CONDITION_TYPE_AND
|
||||
DEFAULT_HIDE_ENTITY = False
|
||||
DEFAULT_INITIAL_STATE = True
|
||||
|
||||
ATTR_LAST_TRIGGERED = 'last_triggered'
|
||||
ATTR_VARIABLES = 'variables'
|
||||
SERVICE_TRIGGER = 'trigger'
|
||||
ATTR_LAST_TRIGGERED = "last_triggered"
|
||||
ATTR_VARIABLES = "variables"
|
||||
SERVICE_TRIGGER = "trigger"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -60,10 +68,10 @@ def _platform_validator(config):
|
|||
"""Validate it is a valid platform."""
|
||||
try:
|
||||
platform = importlib.import_module(
|
||||
'homeassistant.components.automation.{}'.format(
|
||||
config[CONF_PLATFORM]))
|
||||
"homeassistant.components.automation.{}".format(config[CONF_PLATFORM])
|
||||
)
|
||||
except ImportError:
|
||||
raise vol.Invalid('Invalid platform specified') from None
|
||||
raise vol.Invalid("Invalid platform specified") from None
|
||||
|
||||
return platform.TRIGGER_SCHEMA(config)
|
||||
|
||||
|
|
@ -72,17 +80,16 @@ _TRIGGER_SCHEMA = vol.All(
|
|||
cv.ensure_list,
|
||||
[
|
||||
vol.All(
|
||||
vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): str
|
||||
}, extra=vol.ALLOW_EXTRA),
|
||||
_platform_validator
|
||||
),
|
||||
]
|
||||
vol.Schema({vol.Required(CONF_PLATFORM): str}, extra=vol.ALLOW_EXTRA),
|
||||
_platform_validator,
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
_CONDITION_SCHEMA = vol.All(cv.ensure_list, [cv.CONDITION_SCHEMA])
|
||||
|
||||
PLATFORM_SCHEMA = vol.Schema({
|
||||
PLATFORM_SCHEMA = vol.Schema(
|
||||
{
|
||||
# str on purpose
|
||||
CONF_ID: str,
|
||||
CONF_ALIAS: cv.string,
|
||||
|
|
@ -91,16 +98,17 @@ PLATFORM_SCHEMA = vol.Schema({
|
|||
vol.Required(CONF_TRIGGER): _TRIGGER_SCHEMA,
|
||||
vol.Optional(CONF_CONDITION): _CONDITION_SCHEMA,
|
||||
vol.Required(CONF_ACTION): cv.SCRIPT_SCHEMA,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
})
|
||||
SERVICE_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.entity_ids})
|
||||
|
||||
TRIGGER_SERVICE_SCHEMA = vol.Schema({
|
||||
TRIGGER_SERVICE_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Optional(ATTR_VARIABLES, default={}): dict,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
RELOAD_SERVICE_SCHEMA = vol.Schema({})
|
||||
|
||||
|
|
@ -160,8 +168,9 @@ def async_reload(hass):
|
|||
|
||||
async def async_setup(hass, config):
|
||||
"""Set up the automation."""
|
||||
component = EntityComponent(_LOGGER, DOMAIN, hass,
|
||||
group_name=GROUP_NAME_ALL_AUTOMATIONS)
|
||||
component = EntityComponent(
|
||||
_LOGGER, DOMAIN, hass, group_name=GROUP_NAME_ALL_AUTOMATIONS
|
||||
)
|
||||
|
||||
await _async_process_config(hass, config, component)
|
||||
|
||||
|
|
@ -169,10 +178,13 @@ async def async_setup(hass, config):
|
|||
"""Handle automation triggers."""
|
||||
tasks = []
|
||||
for entity in component.async_extract_from_service(service_call):
|
||||
tasks.append(entity.async_trigger(
|
||||
tasks.append(
|
||||
entity.async_trigger(
|
||||
service_call.data.get(ATTR_VARIABLES),
|
||||
skip_condition=True,
|
||||
context=service_call.context))
|
||||
context=service_call.context,
|
||||
)
|
||||
)
|
||||
|
||||
if tasks:
|
||||
await asyncio.wait(tasks, loop=hass.loop)
|
||||
|
|
@ -180,7 +192,7 @@ async def async_setup(hass, config):
|
|||
async def turn_onoff_service_handler(service_call):
|
||||
"""Handle automation turn on/off service calls."""
|
||||
tasks = []
|
||||
method = 'async_{}'.format(service_call.service)
|
||||
method = "async_{}".format(service_call.service)
|
||||
for entity in component.async_extract_from_service(service_call):
|
||||
tasks.append(getattr(entity, method)())
|
||||
|
||||
|
|
@ -207,21 +219,21 @@ async def async_setup(hass, config):
|
|||
await _async_process_config(hass, conf, component)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_TRIGGER, trigger_service_handler,
|
||||
schema=TRIGGER_SERVICE_SCHEMA)
|
||||
DOMAIN, SERVICE_TRIGGER, trigger_service_handler, schema=TRIGGER_SERVICE_SCHEMA
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_RELOAD, reload_service_handler,
|
||||
schema=RELOAD_SERVICE_SCHEMA)
|
||||
DOMAIN, SERVICE_RELOAD, reload_service_handler, schema=RELOAD_SERVICE_SCHEMA
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_TOGGLE, toggle_service_handler,
|
||||
schema=SERVICE_SCHEMA)
|
||||
DOMAIN, SERVICE_TOGGLE, toggle_service_handler, schema=SERVICE_SCHEMA
|
||||
)
|
||||
|
||||
for service in (SERVICE_TURN_ON, SERVICE_TURN_OFF):
|
||||
hass.services.async_register(
|
||||
DOMAIN, service, turn_onoff_service_handler,
|
||||
schema=SERVICE_SCHEMA)
|
||||
DOMAIN, service, turn_onoff_service_handler, schema=SERVICE_SCHEMA
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
|
@ -229,8 +241,16 @@ async def async_setup(hass, config):
|
|||
class AutomationEntity(ToggleEntity):
|
||||
"""Entity to show status of entity."""
|
||||
|
||||
def __init__(self, automation_id, name, async_attach_triggers, cond_func,
|
||||
async_action, hidden, initial_state):
|
||||
def __init__(
|
||||
self,
|
||||
automation_id,
|
||||
name,
|
||||
async_attach_triggers,
|
||||
cond_func,
|
||||
async_action,
|
||||
hidden,
|
||||
initial_state,
|
||||
):
|
||||
"""Initialize an automation entity."""
|
||||
self._id = automation_id
|
||||
self._name = name
|
||||
|
|
@ -255,9 +275,7 @@ class AutomationEntity(ToggleEntity):
|
|||
@property
|
||||
def state_attributes(self):
|
||||
"""Return the entity state attributes."""
|
||||
return {
|
||||
ATTR_LAST_TRIGGERED: self._last_triggered
|
||||
}
|
||||
return {ATTR_LAST_TRIGGERED: self._last_triggered}
|
||||
|
||||
@property
|
||||
def hidden(self) -> bool:
|
||||
|
|
@ -273,33 +291,43 @@ class AutomationEntity(ToggleEntity):
|
|||
"""Startup with initial state or previous state."""
|
||||
if self._initial_state is not None:
|
||||
enable_automation = self._initial_state
|
||||
_LOGGER.debug("Automation %s initial state %s from config "
|
||||
"initial_state", self.entity_id, enable_automation)
|
||||
_LOGGER.debug(
|
||||
"Automation %s initial state %s from config " "initial_state",
|
||||
self.entity_id,
|
||||
enable_automation,
|
||||
)
|
||||
else:
|
||||
state = await async_get_last_state(self.hass, self.entity_id)
|
||||
if state:
|
||||
enable_automation = state.state == STATE_ON
|
||||
self._last_triggered = state.attributes.get('last_triggered')
|
||||
_LOGGER.debug("Automation %s initial state %s from recorder "
|
||||
"last state %s", self.entity_id,
|
||||
enable_automation, state)
|
||||
self._last_triggered = state.attributes.get("last_triggered")
|
||||
_LOGGER.debug(
|
||||
"Automation %s initial state %s from recorder " "last state %s",
|
||||
self.entity_id,
|
||||
enable_automation,
|
||||
state,
|
||||
)
|
||||
else:
|
||||
enable_automation = DEFAULT_INITIAL_STATE
|
||||
_LOGGER.debug("Automation %s initial state %s from default "
|
||||
"initial state", self.entity_id,
|
||||
enable_automation)
|
||||
_LOGGER.debug(
|
||||
"Automation %s initial state %s from default " "initial state",
|
||||
self.entity_id,
|
||||
enable_automation,
|
||||
)
|
||||
|
||||
if not enable_automation:
|
||||
return
|
||||
|
||||
# HomeAssistant is starting up
|
||||
if self.hass.state == CoreState.not_running:
|
||||
|
||||
async def async_enable_automation(event):
|
||||
"""Start automation on startup."""
|
||||
await self.async_enable()
|
||||
|
||||
self.hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_START, async_enable_automation)
|
||||
EVENT_HOMEASSISTANT_START, async_enable_automation
|
||||
)
|
||||
|
||||
# HomeAssistant is running
|
||||
else:
|
||||
|
|
@ -321,8 +349,7 @@ class AutomationEntity(ToggleEntity):
|
|||
self._async_detach_triggers = None
|
||||
await self.async_update_ha_state()
|
||||
|
||||
async def async_trigger(self, variables, skip_condition=False,
|
||||
context=None):
|
||||
async def async_trigger(self, variables, skip_condition=False, context=None):
|
||||
"""Trigger automation.
|
||||
|
||||
This method is a coroutine.
|
||||
|
|
@ -346,7 +373,8 @@ class AutomationEntity(ToggleEntity):
|
|||
return
|
||||
|
||||
self._async_detach_triggers = await self._async_attach_triggers(
|
||||
self.async_trigger)
|
||||
self.async_trigger
|
||||
)
|
||||
await self.async_update_ha_state()
|
||||
|
||||
@property
|
||||
|
|
@ -355,9 +383,7 @@ class AutomationEntity(ToggleEntity):
|
|||
if self._id is None:
|
||||
return None
|
||||
|
||||
return {
|
||||
CONF_ID: self._id
|
||||
}
|
||||
return {CONF_ID: self._id}
|
||||
|
||||
|
||||
async def _async_process_config(hass, config, component):
|
||||
|
|
@ -372,14 +398,12 @@ async def _async_process_config(hass, config, component):
|
|||
|
||||
for list_no, config_block in enumerate(conf):
|
||||
automation_id = config_block.get(CONF_ID)
|
||||
name = config_block.get(CONF_ALIAS) or "{} {}".format(config_key,
|
||||
list_no)
|
||||
name = config_block.get(CONF_ALIAS) or "{} {}".format(config_key, list_no)
|
||||
|
||||
hidden = config_block[CONF_HIDE_ENTITY]
|
||||
initial_state = config_block.get(CONF_INITIAL_STATE)
|
||||
|
||||
action = _async_get_action(hass, config_block.get(CONF_ACTION, {}),
|
||||
name)
|
||||
action = _async_get_action(hass, config_block.get(CONF_ACTION, {}), name)
|
||||
|
||||
if CONF_CONDITION in config_block:
|
||||
cond_func = _async_process_if(hass, config, config_block)
|
||||
|
|
@ -387,17 +411,27 @@ async def _async_process_config(hass, config, component):
|
|||
if cond_func is None:
|
||||
continue
|
||||
else:
|
||||
|
||||
def cond_func(variables):
|
||||
"""Condition will always pass."""
|
||||
return True
|
||||
|
||||
async_attach_triggers = partial(
|
||||
_async_process_trigger, hass, config,
|
||||
config_block.get(CONF_TRIGGER, []), name
|
||||
_async_process_trigger,
|
||||
hass,
|
||||
config,
|
||||
config_block.get(CONF_TRIGGER, []),
|
||||
name,
|
||||
)
|
||||
entity = AutomationEntity(
|
||||
automation_id, name, async_attach_triggers, cond_func, action,
|
||||
hidden, initial_state)
|
||||
automation_id,
|
||||
name,
|
||||
async_attach_triggers,
|
||||
cond_func,
|
||||
action,
|
||||
hidden,
|
||||
initial_state,
|
||||
)
|
||||
|
||||
entities.append(entity)
|
||||
|
||||
|
|
@ -411,9 +445,8 @@ def _async_get_action(hass, config, name):
|
|||
|
||||
async def action(entity_id, variables, context):
|
||||
"""Execute an action."""
|
||||
_LOGGER.info('Executing %s', name)
|
||||
logbook.async_log_entry(
|
||||
hass, name, 'has been triggered', DOMAIN, entity_id)
|
||||
_LOGGER.info("Executing %s", name)
|
||||
logbook.async_log_entry(hass, name, "has been triggered", DOMAIN, entity_id)
|
||||
await script_obj.async_run(variables, context)
|
||||
|
||||
return action
|
||||
|
|
@ -428,7 +461,7 @@ def _async_process_if(hass, config, p_config):
|
|||
try:
|
||||
checks.append(condition.async_from_config(if_config, False))
|
||||
except HomeAssistantError as ex:
|
||||
_LOGGER.warning('Invalid condition: %s', ex)
|
||||
_LOGGER.warning("Invalid condition: %s", ex)
|
||||
return None
|
||||
|
||||
def if_action(variables=None):
|
||||
|
|
@ -447,7 +480,8 @@ async def _async_process_trigger(hass, config, trigger_configs, name, action):
|
|||
|
||||
for conf in trigger_configs:
|
||||
platform = await async_prepare_setup_platform(
|
||||
hass, config, DOMAIN, conf.get(CONF_PLATFORM))
|
||||
hass, config, DOMAIN, conf.get(CONF_PLATFORM)
|
||||
)
|
||||
|
||||
if platform is None:
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -13,25 +13,29 @@ from homeassistant.core import callback
|
|||
from homeassistant.const import CONF_PLATFORM
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
CONF_EVENT_TYPE = 'event_type'
|
||||
CONF_EVENT_DATA = 'event_data'
|
||||
CONF_EVENT_TYPE = "event_type"
|
||||
CONF_EVENT_DATA = "event_data"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
TRIGGER_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): 'event',
|
||||
TRIGGER_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_PLATFORM): "event",
|
||||
vol.Required(CONF_EVENT_TYPE): cv.string,
|
||||
vol.Optional(CONF_EVENT_DATA): dict,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_trigger(hass, config, action):
|
||||
"""Listen for events based on configuration."""
|
||||
event_type = config.get(CONF_EVENT_TYPE)
|
||||
event_data_schema = vol.Schema(
|
||||
config.get(CONF_EVENT_DATA),
|
||||
extra=vol.ALLOW_EXTRA) if config.get(CONF_EVENT_DATA) else None
|
||||
event_data_schema = (
|
||||
vol.Schema(config.get(CONF_EVENT_DATA), extra=vol.ALLOW_EXTRA)
|
||||
if config.get(CONF_EVENT_DATA)
|
||||
else None
|
||||
)
|
||||
|
||||
@callback
|
||||
def handle_event(event):
|
||||
|
|
@ -45,11 +49,11 @@ def async_trigger(hass, config, action):
|
|||
# If event data doesn't match requested schema, skip event
|
||||
return
|
||||
|
||||
hass.async_run_job(action({
|
||||
'trigger': {
|
||||
'platform': 'event',
|
||||
'event': event,
|
||||
},
|
||||
}, context=event.context))
|
||||
hass.async_run_job(
|
||||
action(
|
||||
{"trigger": {"platform": "event", "event": event}},
|
||||
context=event.context,
|
||||
)
|
||||
)
|
||||
|
||||
return hass.bus.async_listen(event_type, handle_event)
|
||||
|
|
|
|||
|
|
@ -10,17 +10,18 @@ import logging
|
|||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback, CoreState
|
||||
from homeassistant.const import (
|
||||
CONF_PLATFORM, CONF_EVENT, EVENT_HOMEASSISTANT_STOP)
|
||||
from homeassistant.const import CONF_PLATFORM, CONF_EVENT, EVENT_HOMEASSISTANT_STOP
|
||||
|
||||
EVENT_START = 'start'
|
||||
EVENT_SHUTDOWN = 'shutdown'
|
||||
EVENT_START = "start"
|
||||
EVENT_SHUTDOWN = "shutdown"
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
TRIGGER_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): 'homeassistant',
|
||||
TRIGGER_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_PLATFORM): "homeassistant",
|
||||
vol.Required(CONF_EVENT): vol.Any(EVENT_START, EVENT_SHUTDOWN),
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
|
|
@ -29,27 +30,24 @@ def async_trigger(hass, config, action):
|
|||
event = config.get(CONF_EVENT)
|
||||
|
||||
if event == EVENT_SHUTDOWN:
|
||||
|
||||
@callback
|
||||
def hass_shutdown(event):
|
||||
"""Execute when Home Assistant is shutting down."""
|
||||
hass.async_run_job(action({
|
||||
'trigger': {
|
||||
'platform': 'homeassistant',
|
||||
'event': event,
|
||||
},
|
||||
}, context=event.context))
|
||||
hass.async_run_job(
|
||||
action(
|
||||
{"trigger": {"platform": "homeassistant", "event": event}},
|
||||
context=event.context,
|
||||
)
|
||||
)
|
||||
|
||||
return hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP,
|
||||
hass_shutdown)
|
||||
return hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, hass_shutdown)
|
||||
|
||||
# Automation are enabled while hass is starting up, fire right away
|
||||
# Check state because a config reload shouldn't trigger it.
|
||||
if hass.state == CoreState.starting:
|
||||
hass.async_run_job(action({
|
||||
'trigger': {
|
||||
'platform': 'homeassistant',
|
||||
'event': event,
|
||||
},
|
||||
}))
|
||||
hass.async_run_job(
|
||||
action({"trigger": {"platform": "homeassistant", "event": event}})
|
||||
)
|
||||
|
||||
return lambda: None
|
||||
|
|
|
|||
|
|
@ -15,22 +15,26 @@ import homeassistant.helpers.config_validation as cv
|
|||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.helpers.event import track_point_in_utc_time
|
||||
|
||||
DEPENDENCIES = ['litejet']
|
||||
DEPENDENCIES = ["litejet"]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_NUMBER = 'number'
|
||||
CONF_HELD_MORE_THAN = 'held_more_than'
|
||||
CONF_HELD_LESS_THAN = 'held_less_than'
|
||||
CONF_NUMBER = "number"
|
||||
CONF_HELD_MORE_THAN = "held_more_than"
|
||||
CONF_HELD_LESS_THAN = "held_less_than"
|
||||
|
||||
TRIGGER_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): 'litejet',
|
||||
TRIGGER_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_PLATFORM): "litejet",
|
||||
vol.Required(CONF_NUMBER): cv.positive_int,
|
||||
vol.Optional(CONF_HELD_MORE_THAN):
|
||||
vol.All(cv.time_period, cv.positive_timedelta),
|
||||
vol.Optional(CONF_HELD_LESS_THAN):
|
||||
vol.All(cv.time_period, cv.positive_timedelta)
|
||||
})
|
||||
vol.Optional(CONF_HELD_MORE_THAN): vol.All(
|
||||
cv.time_period, cv.positive_timedelta
|
||||
),
|
||||
vol.Optional(CONF_HELD_LESS_THAN): vol.All(
|
||||
cv.time_period, cv.positive_timedelta
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
|
|
@ -45,14 +49,17 @@ def async_trigger(hass, config, action):
|
|||
@callback
|
||||
def call_action():
|
||||
"""Call action with right context."""
|
||||
hass.async_run_job(action, {
|
||||
'trigger': {
|
||||
CONF_PLATFORM: 'litejet',
|
||||
hass.async_run_job(
|
||||
action,
|
||||
{
|
||||
"trigger": {
|
||||
CONF_PLATFORM: "litejet",
|
||||
CONF_NUMBER: number,
|
||||
CONF_HELD_MORE_THAN: held_more_than,
|
||||
CONF_HELD_LESS_THAN: held_less_than
|
||||
CONF_HELD_LESS_THAN: held_less_than,
|
||||
}
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
# held_more_than and held_less_than: trigger on released (if in time range)
|
||||
# held_more_than: trigger after pressed with calculation
|
||||
|
|
@ -73,9 +80,8 @@ def async_trigger(hass, config, action):
|
|||
hass.add_job(call_action)
|
||||
if held_more_than is not None and held_less_than is None:
|
||||
cancel_pressed_more_than = track_point_in_utc_time(
|
||||
hass,
|
||||
pressed_more_than_satisfied,
|
||||
dt_util.utcnow() + held_more_than)
|
||||
hass, pressed_more_than_satisfied, dt_util.utcnow() + held_more_than
|
||||
)
|
||||
|
||||
def released():
|
||||
"""Handle the release of the LiteJet switch's button."""
|
||||
|
|
@ -90,8 +96,8 @@ def async_trigger(hass, config, action):
|
|||
if held_more_than is None or held_time > held_more_than:
|
||||
hass.add_job(call_action)
|
||||
|
||||
hass.data['litejet_system'].on_switch_pressed(number, pressed)
|
||||
hass.data['litejet_system'].on_switch_released(number, released)
|
||||
hass.data["litejet_system"].on_switch_pressed(number, pressed)
|
||||
hass.data["litejet_system"].on_switch_released(number, released)
|
||||
|
||||
def async_remove():
|
||||
"""Remove all subscriptions used for this trigger."""
|
||||
|
|
|
|||
|
|
@ -11,18 +11,20 @@ import voluptuous as vol
|
|||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.components import mqtt
|
||||
from homeassistant.const import (CONF_PLATFORM, CONF_PAYLOAD)
|
||||
from homeassistant.const import CONF_PLATFORM, CONF_PAYLOAD
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
DEPENDENCIES = ['mqtt']
|
||||
DEPENDENCIES = ["mqtt"]
|
||||
|
||||
CONF_TOPIC = 'topic'
|
||||
CONF_TOPIC = "topic"
|
||||
|
||||
TRIGGER_SCHEMA = vol.Schema({
|
||||
TRIGGER_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_PLATFORM): mqtt.DOMAIN,
|
||||
vol.Required(CONF_TOPIC): mqtt.valid_subscribe_topic,
|
||||
vol.Optional(CONF_PAYLOAD): cv.string,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
|
|
@ -36,21 +38,18 @@ def async_trigger(hass, config, action):
|
|||
"""Listen for MQTT messages."""
|
||||
if payload is None or payload == msg_payload:
|
||||
data = {
|
||||
'platform': 'mqtt',
|
||||
'topic': msg_topic,
|
||||
'payload': msg_payload,
|
||||
'qos': qos,
|
||||
"platform": "mqtt",
|
||||
"topic": msg_topic,
|
||||
"payload": msg_payload,
|
||||
"qos": qos,
|
||||
}
|
||||
|
||||
try:
|
||||
data['payload_json'] = json.loads(msg_payload)
|
||||
data["payload_json"] = json.loads(msg_payload)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
hass.async_run_job(action, {
|
||||
'trigger': data
|
||||
})
|
||||
hass.async_run_job(action, {"trigger": data})
|
||||
|
||||
remove = yield from mqtt.async_subscribe(
|
||||
hass, topic, mqtt_automation_listener)
|
||||
remove = yield from mqtt.async_subscribe(hass, topic, mqtt_automation_listener)
|
||||
return remove
|
||||
|
|
|
|||
|
|
@ -11,20 +11,29 @@ import voluptuous as vol
|
|||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import (
|
||||
CONF_VALUE_TEMPLATE, CONF_PLATFORM, CONF_ENTITY_ID,
|
||||
CONF_BELOW, CONF_ABOVE, CONF_FOR)
|
||||
from homeassistant.helpers.event import (
|
||||
async_track_state_change, async_track_same_state)
|
||||
CONF_VALUE_TEMPLATE,
|
||||
CONF_PLATFORM,
|
||||
CONF_ENTITY_ID,
|
||||
CONF_BELOW,
|
||||
CONF_ABOVE,
|
||||
CONF_FOR,
|
||||
)
|
||||
from homeassistant.helpers.event import async_track_state_change, async_track_same_state
|
||||
from homeassistant.helpers import condition, config_validation as cv
|
||||
|
||||
TRIGGER_SCHEMA = vol.All(vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): 'numeric_state',
|
||||
TRIGGER_SCHEMA = vol.All(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_PLATFORM): "numeric_state",
|
||||
vol.Required(CONF_ENTITY_ID): cv.entity_ids,
|
||||
vol.Optional(CONF_BELOW): vol.Coerce(float),
|
||||
vol.Optional(CONF_ABOVE): vol.Coerce(float),
|
||||
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_FOR): vol.All(cv.time_period, cv.positive_timedelta),
|
||||
}), cv.has_at_least_one_key(CONF_BELOW, CONF_ABOVE))
|
||||
}
|
||||
),
|
||||
cv.has_at_least_one_key(CONF_BELOW, CONF_ABOVE),
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -50,32 +59,39 @@ def async_trigger(hass, config, action):
|
|||
return False
|
||||
|
||||
variables = {
|
||||
'trigger': {
|
||||
'platform': 'numeric_state',
|
||||
'entity_id': entity,
|
||||
'below': below,
|
||||
'above': above,
|
||||
"trigger": {
|
||||
"platform": "numeric_state",
|
||||
"entity_id": entity,
|
||||
"below": below,
|
||||
"above": above,
|
||||
}
|
||||
}
|
||||
return condition.async_numeric_state(
|
||||
hass, to_s, below, above, value_template, variables)
|
||||
hass, to_s, below, above, value_template, variables
|
||||
)
|
||||
|
||||
@callback
|
||||
def state_automation_listener(entity, from_s, to_s):
|
||||
"""Listen for state changes and calls action."""
|
||||
|
||||
@callback
|
||||
def call_action():
|
||||
"""Call action with right context."""
|
||||
hass.async_run_job(action({
|
||||
'trigger': {
|
||||
'platform': 'numeric_state',
|
||||
'entity_id': entity,
|
||||
'below': below,
|
||||
'above': above,
|
||||
'from_state': from_s,
|
||||
'to_state': to_s,
|
||||
hass.async_run_job(
|
||||
action(
|
||||
{
|
||||
"trigger": {
|
||||
"platform": "numeric_state",
|
||||
"entity_id": entity,
|
||||
"below": below,
|
||||
"above": above,
|
||||
"from_state": from_s,
|
||||
"to_state": to_s,
|
||||
}
|
||||
}, context=to_s.context))
|
||||
},
|
||||
context=to_s.context,
|
||||
)
|
||||
)
|
||||
|
||||
matching = check_numeric_state(entity, from_s, to_s)
|
||||
|
||||
|
|
@ -86,13 +102,16 @@ def async_trigger(hass, config, action):
|
|||
|
||||
if time_delta:
|
||||
unsub_track_same[entity] = async_track_same_state(
|
||||
hass, time_delta, call_action, entity_ids=entity_id,
|
||||
async_check_same_func=check_numeric_state)
|
||||
hass,
|
||||
time_delta,
|
||||
call_action,
|
||||
entity_ids=entity_id,
|
||||
async_check_same_func=check_numeric_state,
|
||||
)
|
||||
else:
|
||||
call_action()
|
||||
|
||||
unsub = async_track_state_change(
|
||||
hass, entity_id, state_automation_listener)
|
||||
unsub = async_track_state_change(hass, entity_id, state_automation_listener)
|
||||
|
||||
@callback
|
||||
def async_remove():
|
||||
|
|
|
|||
|
|
@ -9,22 +9,26 @@ import voluptuous as vol
|
|||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import MATCH_ALL, CONF_PLATFORM, CONF_FOR
|
||||
from homeassistant.helpers.event import (
|
||||
async_track_state_change, async_track_same_state)
|
||||
from homeassistant.helpers.event import async_track_state_change, async_track_same_state
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
CONF_ENTITY_ID = 'entity_id'
|
||||
CONF_FROM = 'from'
|
||||
CONF_TO = 'to'
|
||||
CONF_ENTITY_ID = "entity_id"
|
||||
CONF_FROM = "from"
|
||||
CONF_TO = "to"
|
||||
|
||||
TRIGGER_SCHEMA = vol.All(vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): 'state',
|
||||
TRIGGER_SCHEMA = vol.All(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_PLATFORM): "state",
|
||||
vol.Required(CONF_ENTITY_ID): cv.entity_ids,
|
||||
# These are str on purpose. Want to catch YAML conversions
|
||||
vol.Optional(CONF_FROM): str,
|
||||
vol.Optional(CONF_TO): str,
|
||||
vol.Optional(CONF_FOR): vol.All(cv.time_period, cv.positive_timedelta),
|
||||
}), cv.key_dependency(CONF_FOR, CONF_TO))
|
||||
}
|
||||
),
|
||||
cv.key_dependency(CONF_FOR, CONF_TO),
|
||||
)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
|
|
@ -34,28 +38,38 @@ def async_trigger(hass, config, action):
|
|||
from_state = config.get(CONF_FROM, MATCH_ALL)
|
||||
to_state = config.get(CONF_TO, MATCH_ALL)
|
||||
time_delta = config.get(CONF_FOR)
|
||||
match_all = (from_state == MATCH_ALL and to_state == MATCH_ALL)
|
||||
match_all = from_state == MATCH_ALL and to_state == MATCH_ALL
|
||||
unsub_track_same = {}
|
||||
|
||||
@callback
|
||||
def state_automation_listener(entity, from_s, to_s):
|
||||
"""Listen for state changes and calls action."""
|
||||
|
||||
@callback
|
||||
def call_action():
|
||||
"""Call action with right context."""
|
||||
hass.async_run_job(action({
|
||||
'trigger': {
|
||||
'platform': 'state',
|
||||
'entity_id': entity,
|
||||
'from_state': from_s,
|
||||
'to_state': to_s,
|
||||
'for': time_delta,
|
||||
hass.async_run_job(
|
||||
action(
|
||||
{
|
||||
"trigger": {
|
||||
"platform": "state",
|
||||
"entity_id": entity,
|
||||
"from_state": from_s,
|
||||
"to_state": to_s,
|
||||
"for": time_delta,
|
||||
}
|
||||
}, context=to_s.context))
|
||||
},
|
||||
context=to_s.context,
|
||||
)
|
||||
)
|
||||
|
||||
# Ignore changes to state attributes if from/to is in use
|
||||
if (not match_all and from_s is not None and to_s is not None and
|
||||
from_s.state == to_s.state):
|
||||
if (
|
||||
not match_all
|
||||
and from_s is not None
|
||||
and to_s is not None
|
||||
and from_s.state == to_s.state
|
||||
):
|
||||
return
|
||||
|
||||
if not time_delta:
|
||||
|
|
@ -63,12 +77,16 @@ def async_trigger(hass, config, action):
|
|||
return
|
||||
|
||||
unsub_track_same[entity] = async_track_same_state(
|
||||
hass, time_delta, call_action,
|
||||
hass,
|
||||
time_delta,
|
||||
call_action,
|
||||
lambda _, _2, to_state: to_state.state == to_s.state,
|
||||
entity_ids=entity_id)
|
||||
entity_ids=entity_id,
|
||||
)
|
||||
|
||||
unsub = async_track_state_change(
|
||||
hass, entity_id, state_automation_listener, from_state, to_state)
|
||||
hass, entity_id, state_automation_listener, from_state, to_state
|
||||
)
|
||||
|
||||
@callback
|
||||
def async_remove():
|
||||
|
|
|
|||
|
|
@ -12,17 +12,23 @@ import voluptuous as vol
|
|||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import (
|
||||
CONF_EVENT, CONF_OFFSET, CONF_PLATFORM, SUN_EVENT_SUNRISE)
|
||||
CONF_EVENT,
|
||||
CONF_OFFSET,
|
||||
CONF_PLATFORM,
|
||||
SUN_EVENT_SUNRISE,
|
||||
)
|
||||
from homeassistant.helpers.event import async_track_sunrise, async_track_sunset
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
TRIGGER_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): 'sun',
|
||||
TRIGGER_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_PLATFORM): "sun",
|
||||
vol.Required(CONF_EVENT): cv.sun_event,
|
||||
vol.Required(CONF_OFFSET, default=timedelta(0)): cv.time_period,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
|
|
@ -34,13 +40,9 @@ def async_trigger(hass, config, action):
|
|||
@callback
|
||||
def call_action():
|
||||
"""Call action with right context."""
|
||||
hass.async_run_job(action, {
|
||||
'trigger': {
|
||||
'platform': 'sun',
|
||||
'event': event,
|
||||
'offset': offset,
|
||||
},
|
||||
})
|
||||
hass.async_run_job(
|
||||
action, {"trigger": {"platform": "sun", "event": event, "offset": offset}}
|
||||
)
|
||||
|
||||
if event == SUN_EVENT_SUNRISE:
|
||||
return async_track_sunrise(hass, call_action, offset)
|
||||
|
|
|
|||
|
|
@ -17,10 +17,12 @@ import homeassistant.helpers.config_validation as cv
|
|||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
TRIGGER_SCHEMA = IF_ACTION_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): 'template',
|
||||
TRIGGER_SCHEMA = IF_ACTION_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_PLATFORM): "template",
|
||||
vol.Required(CONF_VALUE_TEMPLATE): cv.template,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
|
|
@ -32,13 +34,18 @@ def async_trigger(hass, config, action):
|
|||
@callback
|
||||
def template_listener(entity_id, from_s, to_s):
|
||||
"""Listen for state changes and calls action."""
|
||||
hass.async_run_job(action({
|
||||
'trigger': {
|
||||
'platform': 'template',
|
||||
'entity_id': entity_id,
|
||||
'from_state': from_s,
|
||||
'to_state': to_s,
|
||||
hass.async_run_job(
|
||||
action(
|
||||
{
|
||||
"trigger": {
|
||||
"platform": "template",
|
||||
"entity_id": entity_id,
|
||||
"from_state": from_s,
|
||||
"to_state": to_s,
|
||||
}
|
||||
},
|
||||
}, context=to_s.context))
|
||||
context=to_s.context,
|
||||
)
|
||||
)
|
||||
|
||||
return async_track_template(hass, value_template, template_listener)
|
||||
|
|
|
|||
|
|
@ -14,19 +14,24 @@ from homeassistant.const import CONF_AT, CONF_PLATFORM
|
|||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.event import async_track_time_change
|
||||
|
||||
CONF_HOURS = 'hours'
|
||||
CONF_MINUTES = 'minutes'
|
||||
CONF_SECONDS = 'seconds'
|
||||
CONF_HOURS = "hours"
|
||||
CONF_MINUTES = "minutes"
|
||||
CONF_SECONDS = "seconds"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
TRIGGER_SCHEMA = vol.All(vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): 'time',
|
||||
TRIGGER_SCHEMA = vol.All(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_PLATFORM): "time",
|
||||
CONF_AT: cv.time,
|
||||
CONF_HOURS: vol.Any(vol.Coerce(int), vol.Coerce(str)),
|
||||
CONF_MINUTES: vol.Any(vol.Coerce(int), vol.Coerce(str)),
|
||||
CONF_SECONDS: vol.Any(vol.Coerce(int), vol.Coerce(str)),
|
||||
}), cv.has_at_least_one_key(CONF_HOURS, CONF_MINUTES, CONF_SECONDS, CONF_AT))
|
||||
}
|
||||
),
|
||||
cv.has_at_least_one_key(CONF_HOURS, CONF_MINUTES, CONF_SECONDS, CONF_AT),
|
||||
)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
|
|
@ -43,12 +48,8 @@ def async_trigger(hass, config, action):
|
|||
@callback
|
||||
def time_automation_listener(now):
|
||||
"""Listen for time changes and calls action."""
|
||||
hass.async_run_job(action, {
|
||||
'trigger': {
|
||||
'platform': 'time',
|
||||
'now': now,
|
||||
},
|
||||
})
|
||||
hass.async_run_job(action, {"trigger": {"platform": "time", "now": now}})
|
||||
|
||||
return async_track_time_change(hass, time_automation_listener,
|
||||
hour=hours, minute=minutes, second=seconds)
|
||||
return async_track_time_change(
|
||||
hass, time_automation_listener, hour=hours, minute=minutes, second=seconds
|
||||
)
|
||||
|
|
|
|||
|
|
@ -9,22 +9,29 @@ import voluptuous as vol
|
|||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import (
|
||||
CONF_EVENT, CONF_ENTITY_ID, CONF_ZONE, MATCH_ALL, CONF_PLATFORM)
|
||||
CONF_EVENT,
|
||||
CONF_ENTITY_ID,
|
||||
CONF_ZONE,
|
||||
MATCH_ALL,
|
||||
CONF_PLATFORM,
|
||||
)
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
from homeassistant.helpers import (
|
||||
condition, config_validation as cv, location)
|
||||
from homeassistant.helpers import condition, config_validation as cv, location
|
||||
|
||||
EVENT_ENTER = 'enter'
|
||||
EVENT_LEAVE = 'leave'
|
||||
EVENT_ENTER = "enter"
|
||||
EVENT_LEAVE = "leave"
|
||||
DEFAULT_EVENT = EVENT_ENTER
|
||||
|
||||
TRIGGER_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): 'zone',
|
||||
TRIGGER_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_PLATFORM): "zone",
|
||||
vol.Required(CONF_ENTITY_ID): cv.entity_ids,
|
||||
vol.Required(CONF_ZONE): cv.entity_id,
|
||||
vol.Required(CONF_EVENT, default=DEFAULT_EVENT):
|
||||
vol.Any(EVENT_ENTER, EVENT_LEAVE),
|
||||
})
|
||||
vol.Required(CONF_EVENT, default=DEFAULT_EVENT): vol.Any(
|
||||
EVENT_ENTER, EVENT_LEAVE
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
|
|
@ -37,8 +44,11 @@ def async_trigger(hass, config, action):
|
|||
@callback
|
||||
def zone_automation_listener(entity, from_s, to_s):
|
||||
"""Listen for state changes and calls action."""
|
||||
if from_s and not location.has_location(from_s) or \
|
||||
not location.has_location(to_s):
|
||||
if (
|
||||
from_s
|
||||
and not location.has_location(from_s)
|
||||
or not location.has_location(to_s)
|
||||
):
|
||||
return
|
||||
|
||||
zone_state = hass.states.get(zone_entity_id)
|
||||
|
|
@ -49,18 +59,30 @@ def async_trigger(hass, config, action):
|
|||
to_match = condition.zone(hass, zone_state, to_s)
|
||||
|
||||
# pylint: disable=too-many-boolean-expressions
|
||||
if event == EVENT_ENTER and not from_match and to_match or \
|
||||
event == EVENT_LEAVE and from_match and not to_match:
|
||||
hass.async_run_job(action({
|
||||
'trigger': {
|
||||
'platform': 'zone',
|
||||
'entity_id': entity,
|
||||
'from_state': from_s,
|
||||
'to_state': to_s,
|
||||
'zone': zone_state,
|
||||
'event': event,
|
||||
if (
|
||||
event == EVENT_ENTER
|
||||
and not from_match
|
||||
and to_match
|
||||
or event == EVENT_LEAVE
|
||||
and from_match
|
||||
and not to_match
|
||||
):
|
||||
hass.async_run_job(
|
||||
action(
|
||||
{
|
||||
"trigger": {
|
||||
"platform": "zone",
|
||||
"entity_id": entity,
|
||||
"from_state": from_s,
|
||||
"to_state": to_s,
|
||||
"zone": zone_state,
|
||||
"event": event,
|
||||
}
|
||||
},
|
||||
}, context=to_s.context))
|
||||
context=to_s.context,
|
||||
)
|
||||
)
|
||||
|
||||
return async_track_state_change(hass, entity_id, zone_automation_listener,
|
||||
MATCH_ALL, MATCH_ALL)
|
||||
return async_track_state_change(
|
||||
hass, entity_id, zone_automation_listener, MATCH_ALL, MATCH_ALL
|
||||
)
|
||||
|
|
|
|||
|
|
@ -10,67 +10,76 @@ import voluptuous as vol
|
|||
|
||||
from homeassistant.components.discovery import SERVICE_AXIS
|
||||
from homeassistant.const import (
|
||||
ATTR_LOCATION, ATTR_TRIPPED, CONF_EVENT, CONF_HOST, CONF_INCLUDE,
|
||||
CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_TRIGGER_TIME, CONF_USERNAME,
|
||||
EVENT_HOMEASSISTANT_STOP)
|
||||
ATTR_LOCATION,
|
||||
ATTR_TRIPPED,
|
||||
CONF_EVENT,
|
||||
CONF_HOST,
|
||||
CONF_INCLUDE,
|
||||
CONF_NAME,
|
||||
CONF_PASSWORD,
|
||||
CONF_PORT,
|
||||
CONF_TRIGGER_TIME,
|
||||
CONF_USERNAME,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers.dispatcher import dispatcher_send
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.util.json import load_json, save_json
|
||||
|
||||
REQUIREMENTS = ['axis==14']
|
||||
REQUIREMENTS = ["axis==14"]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'axis'
|
||||
CONFIG_FILE = 'axis.conf'
|
||||
DOMAIN = "axis"
|
||||
CONFIG_FILE = "axis.conf"
|
||||
|
||||
AXIS_DEVICES = {}
|
||||
|
||||
EVENT_TYPES = ['motion', 'vmd3', 'pir', 'sound',
|
||||
'daynight', 'tampering', 'input']
|
||||
EVENT_TYPES = ["motion", "vmd3", "pir", "sound", "daynight", "tampering", "input"]
|
||||
|
||||
PLATFORMS = ['camera']
|
||||
PLATFORMS = ["camera"]
|
||||
|
||||
AXIS_INCLUDE = EVENT_TYPES + PLATFORMS
|
||||
|
||||
AXIS_DEFAULT_HOST = '192.168.0.90'
|
||||
AXIS_DEFAULT_USERNAME = 'root'
|
||||
AXIS_DEFAULT_PASSWORD = 'pass'
|
||||
AXIS_DEFAULT_HOST = "192.168.0.90"
|
||||
AXIS_DEFAULT_USERNAME = "root"
|
||||
AXIS_DEFAULT_PASSWORD = "pass"
|
||||
|
||||
DEVICE_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_INCLUDE):
|
||||
vol.All(cv.ensure_list, [vol.In(AXIS_INCLUDE)]),
|
||||
DEVICE_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_INCLUDE): vol.All(cv.ensure_list, [vol.In(AXIS_INCLUDE)]),
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_HOST, default=AXIS_DEFAULT_HOST): cv.string,
|
||||
vol.Optional(CONF_USERNAME, default=AXIS_DEFAULT_USERNAME): cv.string,
|
||||
vol.Optional(CONF_PASSWORD, default=AXIS_DEFAULT_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_TRIGGER_TIME, default=0): cv.positive_int,
|
||||
vol.Optional(CONF_PORT, default=80): cv.positive_int,
|
||||
vol.Optional(ATTR_LOCATION, default=''): cv.string,
|
||||
})
|
||||
vol.Optional(ATTR_LOCATION, default=""): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
cv.slug: DEVICE_SCHEMA,
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{DOMAIN: vol.Schema({cv.slug: DEVICE_SCHEMA})}, extra=vol.ALLOW_EXTRA
|
||||
)
|
||||
|
||||
SERVICE_VAPIX_CALL = 'vapix_call'
|
||||
SERVICE_VAPIX_CALL_RESPONSE = 'vapix_call_response'
|
||||
SERVICE_CGI = 'cgi'
|
||||
SERVICE_ACTION = 'action'
|
||||
SERVICE_PARAM = 'param'
|
||||
SERVICE_DEFAULT_CGI = 'param.cgi'
|
||||
SERVICE_DEFAULT_ACTION = 'update'
|
||||
SERVICE_VAPIX_CALL = "vapix_call"
|
||||
SERVICE_VAPIX_CALL_RESPONSE = "vapix_call_response"
|
||||
SERVICE_CGI = "cgi"
|
||||
SERVICE_ACTION = "action"
|
||||
SERVICE_PARAM = "param"
|
||||
SERVICE_DEFAULT_CGI = "param.cgi"
|
||||
SERVICE_DEFAULT_ACTION = "update"
|
||||
|
||||
SERVICE_SCHEMA = vol.Schema({
|
||||
SERVICE_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Required(SERVICE_PARAM): cv.string,
|
||||
vol.Optional(SERVICE_CGI, default=SERVICE_DEFAULT_CGI): cv.string,
|
||||
vol.Optional(SERVICE_ACTION, default=SERVICE_DEFAULT_ACTION): cv.string,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def request_configuration(hass, config, name, host, serialnumber):
|
||||
|
|
@ -80,8 +89,7 @@ def request_configuration(hass, config, name, host, serialnumber):
|
|||
def configuration_callback(callback_data):
|
||||
"""Call when configuration is submitted."""
|
||||
if CONF_INCLUDE not in callback_data:
|
||||
configurator.notify_errors(
|
||||
request_id, "Functionality mandatory.")
|
||||
configurator.notify_errors(request_id, "Functionality mandatory.")
|
||||
return False
|
||||
|
||||
callback_data[CONF_INCLUDE] = callback_data[CONF_INCLUDE].split()
|
||||
|
|
@ -93,58 +101,58 @@ def request_configuration(hass, config, name, host, serialnumber):
|
|||
try:
|
||||
device_config = DEVICE_SCHEMA(callback_data)
|
||||
except vol.Invalid:
|
||||
configurator.notify_errors(
|
||||
request_id, "Bad input, please check spelling.")
|
||||
configurator.notify_errors(request_id, "Bad input, please check spelling.")
|
||||
return False
|
||||
|
||||
if setup_device(hass, config, device_config):
|
||||
del device_config['events']
|
||||
del device_config['signal']
|
||||
del device_config["events"]
|
||||
del device_config["signal"]
|
||||
config_file = load_json(hass.config.path(CONFIG_FILE))
|
||||
config_file[serialnumber] = dict(device_config)
|
||||
save_json(hass.config.path(CONFIG_FILE), config_file)
|
||||
configurator.request_done(request_id)
|
||||
else:
|
||||
configurator.notify_errors(
|
||||
request_id, "Failed to register, please try again.")
|
||||
request_id, "Failed to register, please try again."
|
||||
)
|
||||
return False
|
||||
|
||||
title = '{} ({})'.format(name, host)
|
||||
title = "{} ({})".format(name, host)
|
||||
request_id = configurator.request_config(
|
||||
title, configuration_callback,
|
||||
description='Functionality: ' + str(AXIS_INCLUDE),
|
||||
title,
|
||||
configuration_callback,
|
||||
description="Functionality: " + str(AXIS_INCLUDE),
|
||||
entity_picture="/static/images/logo_axis.png",
|
||||
link_name='Axis platform documentation',
|
||||
link_url='https://home-assistant.io/components/axis/',
|
||||
link_name="Axis platform documentation",
|
||||
link_url="https://home-assistant.io/components/axis/",
|
||||
submit_caption="Confirm",
|
||||
fields=[
|
||||
{'id': CONF_NAME,
|
||||
'name': "Device name",
|
||||
'type': 'text'},
|
||||
{'id': CONF_USERNAME,
|
||||
'name': "User name",
|
||||
'type': 'text'},
|
||||
{'id': CONF_PASSWORD,
|
||||
'name': 'Password',
|
||||
'type': 'password'},
|
||||
{'id': CONF_INCLUDE,
|
||||
'name': "Device functionality (space separated list)",
|
||||
'type': 'text'},
|
||||
{'id': ATTR_LOCATION,
|
||||
'name': "Physical location of device (optional)",
|
||||
'type': 'text'},
|
||||
{'id': CONF_PORT,
|
||||
'name': "HTTP port (default=80)",
|
||||
'type': 'number'},
|
||||
{'id': CONF_TRIGGER_TIME,
|
||||
'name': "Sensor update interval (optional)",
|
||||
'type': 'number'},
|
||||
]
|
||||
{"id": CONF_NAME, "name": "Device name", "type": "text"},
|
||||
{"id": CONF_USERNAME, "name": "User name", "type": "text"},
|
||||
{"id": CONF_PASSWORD, "name": "Password", "type": "password"},
|
||||
{
|
||||
"id": CONF_INCLUDE,
|
||||
"name": "Device functionality (space separated list)",
|
||||
"type": "text",
|
||||
},
|
||||
{
|
||||
"id": ATTR_LOCATION,
|
||||
"name": "Physical location of device (optional)",
|
||||
"type": "text",
|
||||
},
|
||||
{"id": CONF_PORT, "name": "HTTP port (default=80)", "type": "number"},
|
||||
{
|
||||
"id": CONF_TRIGGER_TIME,
|
||||
"name": "Sensor update interval (optional)",
|
||||
"type": "number",
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Set up for Axis devices."""
|
||||
|
||||
def _shutdown(call):
|
||||
"""Stop the event stream on shutdown."""
|
||||
for serialnumber, device in AXIS_DEVICES.items():
|
||||
|
|
@ -156,8 +164,8 @@ def setup(hass, config):
|
|||
def axis_device_discovered(service, discovery_info):
|
||||
"""Call when axis devices has been found."""
|
||||
host = discovery_info[CONF_HOST]
|
||||
name = discovery_info['hostname']
|
||||
serialnumber = discovery_info['properties']['macaddress']
|
||||
name = discovery_info["hostname"]
|
||||
serialnumber = discovery_info["properties"]["macaddress"]
|
||||
|
||||
if serialnumber not in AXIS_DEVICES:
|
||||
config_file = load_json(hass.config.path(CONFIG_FILE))
|
||||
|
|
@ -170,8 +178,7 @@ def setup(hass, config):
|
|||
_LOGGER.error("Bad data from %s. %s", CONFIG_FILE, err)
|
||||
return False
|
||||
if not setup_device(hass, config, device_config):
|
||||
_LOGGER.error(
|
||||
"Couldn't set up %s", device_config[CONF_NAME])
|
||||
_LOGGER.error("Couldn't set up %s", device_config[CONF_NAME])
|
||||
else:
|
||||
# New device, create configuration request for UI
|
||||
request_configuration(hass, config, name, host, serialnumber)
|
||||
|
|
@ -179,7 +186,7 @@ def setup(hass, config):
|
|||
# Device already registered, but on a different IP
|
||||
device = AXIS_DEVICES[serialnumber]
|
||||
device.config.host = host
|
||||
dispatcher_send(hass, DOMAIN + '_' + device.name + '_new_ip', host)
|
||||
dispatcher_send(hass, DOMAIN + "_" + device.name + "_new_ip", host)
|
||||
|
||||
# Register discovery service
|
||||
discovery.listen(hass, SERVICE_AXIS, axis_device_discovered)
|
||||
|
|
@ -199,7 +206,8 @@ def setup(hass, config):
|
|||
response = device.vapix.do_request(
|
||||
call.data[SERVICE_CGI],
|
||||
call.data[SERVICE_ACTION],
|
||||
call.data[SERVICE_PARAM])
|
||||
call.data[SERVICE_PARAM],
|
||||
)
|
||||
hass.bus.fire(SERVICE_VAPIX_CALL_RESPONSE, response)
|
||||
return True
|
||||
_LOGGER.info("Couldn't find device %s", call.data[CONF_NAME])
|
||||
|
|
@ -207,7 +215,8 @@ def setup(hass, config):
|
|||
|
||||
# Register service with Home Assistant.
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_VAPIX_CALL, vapix_service, schema=SERVICE_SCHEMA)
|
||||
DOMAIN, SERVICE_VAPIX_CALL, vapix_service, schema=SERVICE_SCHEMA
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
|
|
@ -217,21 +226,19 @@ def setup_device(hass, config, device_config):
|
|||
|
||||
def signal_callback(action, event):
|
||||
"""Call to configure events when initialized on event stream."""
|
||||
if action == 'add':
|
||||
if action == "add":
|
||||
event_config = {
|
||||
CONF_EVENT: event,
|
||||
CONF_NAME: device_config[CONF_NAME],
|
||||
ATTR_LOCATION: device_config[ATTR_LOCATION],
|
||||
CONF_TRIGGER_TIME: device_config[CONF_TRIGGER_TIME]
|
||||
CONF_TRIGGER_TIME: device_config[CONF_TRIGGER_TIME],
|
||||
}
|
||||
component = event.event_platform
|
||||
discovery.load_platform(
|
||||
hass, component, DOMAIN, event_config, config)
|
||||
discovery.load_platform(hass, component, DOMAIN, event_config, config)
|
||||
|
||||
event_types = list(filter(lambda x: x in device_config[CONF_INCLUDE],
|
||||
EVENT_TYPES))
|
||||
device_config['events'] = event_types
|
||||
device_config['signal'] = signal_callback
|
||||
event_types = list(filter(lambda x: x in device_config[CONF_INCLUDE], EVENT_TYPES))
|
||||
device_config["events"] = event_types
|
||||
device_config["signal"] = signal_callback
|
||||
device = AxisDevice(hass.loop, **device_config)
|
||||
device.name = device_config[CONF_NAME]
|
||||
|
||||
|
|
@ -241,16 +248,15 @@ def setup_device(hass, config, device_config):
|
|||
return False
|
||||
|
||||
for component in device_config[CONF_INCLUDE]:
|
||||
if component == 'camera':
|
||||
if component == "camera":
|
||||
camera_config = {
|
||||
CONF_NAME: device_config[CONF_NAME],
|
||||
CONF_HOST: device_config[CONF_HOST],
|
||||
CONF_PORT: device_config[CONF_PORT],
|
||||
CONF_USERNAME: device_config[CONF_USERNAME],
|
||||
CONF_PASSWORD: device_config[CONF_PASSWORD]
|
||||
CONF_PASSWORD: device_config[CONF_PASSWORD],
|
||||
}
|
||||
discovery.load_platform(
|
||||
hass, component, DOMAIN, camera_config, config)
|
||||
discovery.load_platform(hass, component, DOMAIN, camera_config, config)
|
||||
|
||||
AXIS_DEVICES[device.serial_number] = device
|
||||
if event_types:
|
||||
|
|
@ -264,9 +270,9 @@ class AxisDeviceEvent(Entity):
|
|||
def __init__(self, event_config):
|
||||
"""Initialize the event."""
|
||||
self.axis_event = event_config[CONF_EVENT]
|
||||
self._name = '{}_{}_{}'.format(
|
||||
event_config[CONF_NAME], self.axis_event.event_type,
|
||||
self.axis_event.id)
|
||||
self._name = "{}_{}_{}".format(
|
||||
event_config[CONF_NAME], self.axis_event.event_type, self.axis_event.id
|
||||
)
|
||||
self.location = event_config[ATTR_LOCATION]
|
||||
self.axis_event.callback = self._update_callback
|
||||
|
||||
|
|
@ -295,7 +301,7 @@ class AxisDeviceEvent(Entity):
|
|||
attr = {}
|
||||
|
||||
tripped = self.axis_event.is_tripped
|
||||
attr[ATTR_TRIPPED] = 'True' if tripped else 'False'
|
||||
attr[ATTR_TRIPPED] = "True" if tripped else "False"
|
||||
|
||||
attr[ATTR_LOCATION] = self.location
|
||||
|
||||
|
|
|
|||
|
|
@ -6,14 +6,13 @@ https://home-assistant.io/components/bbb_gpio/
|
|||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP
|
||||
|
||||
REQUIREMENTS = ['Adafruit_BBIO==1.0.0']
|
||||
REQUIREMENTS = ["Adafruit_BBIO==1.0.0"]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'bbb_gpio'
|
||||
DOMAIN = "bbb_gpio"
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
|
|
@ -37,6 +36,7 @@ def setup_output(pin):
|
|||
"""Set up a GPIO as output."""
|
||||
# pylint: disable=import-error
|
||||
from Adafruit_BBIO import GPIO
|
||||
|
||||
GPIO.setup(pin, GPIO.OUT)
|
||||
|
||||
|
||||
|
|
@ -44,15 +44,15 @@ def setup_input(pin, pull_mode):
|
|||
"""Set up a GPIO as input."""
|
||||
# pylint: disable=import-error
|
||||
from Adafruit_BBIO import GPIO
|
||||
GPIO.setup(pin, GPIO.IN,
|
||||
GPIO.PUD_DOWN if pull_mode == 'DOWN'
|
||||
else GPIO.PUD_UP)
|
||||
|
||||
GPIO.setup(pin, GPIO.IN, GPIO.PUD_DOWN if pull_mode == "DOWN" else GPIO.PUD_UP)
|
||||
|
||||
|
||||
def write_output(pin, value):
|
||||
"""Write a value to a GPIO."""
|
||||
# pylint: disable=import-error
|
||||
from Adafruit_BBIO import GPIO
|
||||
|
||||
GPIO.output(pin, value)
|
||||
|
||||
|
||||
|
|
@ -60,6 +60,7 @@ def read_input(pin):
|
|||
"""Read a value from a GPIO."""
|
||||
# pylint: disable=import-error
|
||||
from Adafruit_BBIO import GPIO
|
||||
|
||||
return GPIO.input(pin) is GPIO.HIGH
|
||||
|
||||
|
||||
|
|
@ -67,5 +68,5 @@ def edge_detect(pin, event_callback, bounce):
|
|||
"""Add detection for RISING and FALLING events."""
|
||||
# pylint: disable=import-error
|
||||
from Adafruit_BBIO import GPIO
|
||||
GPIO.add_event_detect(
|
||||
pin, GPIO.BOTH, callback=event_callback, bouncetime=bounce)
|
||||
|
||||
GPIO.add_event_detect(pin, GPIO.BOTH, callback=event_callback, bouncetime=bounce)
|
||||
|
|
|
|||
|
|
@ -12,37 +12,37 @@ import voluptuous as vol
|
|||
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.const import (STATE_ON, STATE_OFF)
|
||||
from homeassistant.const import STATE_ON, STATE_OFF
|
||||
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
||||
|
||||
DOMAIN = 'binary_sensor'
|
||||
DOMAIN = "binary_sensor"
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
ENTITY_ID_FORMAT = DOMAIN + ".{}"
|
||||
DEVICE_CLASSES = [
|
||||
'battery', # On means low, Off means normal
|
||||
'cold', # On means cold, Off means normal
|
||||
'connectivity', # On means connected, Off means disconnected
|
||||
'door', # On means open, Off means closed
|
||||
'garage_door', # On means open, Off means closed
|
||||
'gas', # On means gas detected, Off means no gas (clear)
|
||||
'heat', # On means hot, Off means normal
|
||||
'light', # On means light detected, Off means no light
|
||||
'lock', # On means open (unlocked), Off means closed (locked)
|
||||
'moisture', # On means wet, Off means dry
|
||||
'motion', # On means motion detected, Off means no motion (clear)
|
||||
'moving', # On means moving, Off means not moving (stopped)
|
||||
'occupancy', # On means occupied, Off means not occupied (clear)
|
||||
'opening', # On means open, Off means closed
|
||||
'plug', # On means plugged in, Off means unplugged
|
||||
'power', # On means power detected, Off means no power
|
||||
'presence', # On means home, Off means away
|
||||
'problem', # On means problem detected, Off means no problem (OK)
|
||||
'safety', # On means unsafe, Off means safe
|
||||
'smoke', # On means smoke detected, Off means no smoke (clear)
|
||||
'sound', # On means sound detected, Off means no sound (clear)
|
||||
'vibration', # On means vibration detected, Off means no vibration
|
||||
'window', # On means open, Off means closed
|
||||
"battery", # On means low, Off means normal
|
||||
"cold", # On means cold, Off means normal
|
||||
"connectivity", # On means connected, Off means disconnected
|
||||
"door", # On means open, Off means closed
|
||||
"garage_door", # On means open, Off means closed
|
||||
"gas", # On means gas detected, Off means no gas (clear)
|
||||
"heat", # On means hot, Off means normal
|
||||
"light", # On means light detected, Off means no light
|
||||
"lock", # On means open (unlocked), Off means closed (locked)
|
||||
"moisture", # On means wet, Off means dry
|
||||
"motion", # On means motion detected, Off means no motion (clear)
|
||||
"moving", # On means moving, Off means not moving (stopped)
|
||||
"occupancy", # On means occupied, Off means not occupied (clear)
|
||||
"opening", # On means open, Off means closed
|
||||
"plug", # On means plugged in, Off means unplugged
|
||||
"power", # On means power detected, Off means no power
|
||||
"presence", # On means home, Off means away
|
||||
"problem", # On means problem detected, Off means no problem (OK)
|
||||
"safety", # On means unsafe, Off means safe
|
||||
"smoke", # On means smoke detected, Off means no smoke (clear)
|
||||
"sound", # On means sound detected, Off means no sound (clear)
|
||||
"vibration", # On means vibration detected, Off means no vibration
|
||||
"window", # On means open, Off means closed
|
||||
]
|
||||
|
||||
DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES))
|
||||
|
|
@ -51,7 +51,8 @@ DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES))
|
|||
async def async_setup(hass, config):
|
||||
"""Track states and offer events for binary sensors."""
|
||||
component = hass.data[DOMAIN] = EntityComponent(
|
||||
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
|
||||
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL
|
||||
)
|
||||
|
||||
await component.async_setup(config)
|
||||
return True
|
||||
|
|
|
|||
|
|
@ -6,12 +6,15 @@ https://home-assistant.io/components/binary_sensor.abode/
|
|||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.abode import (AbodeDevice, AbodeAutomation,
|
||||
DOMAIN as ABODE_DOMAIN)
|
||||
from homeassistant.components.abode import (
|
||||
AbodeDevice,
|
||||
AbodeAutomation,
|
||||
DOMAIN as ABODE_DOMAIN,
|
||||
)
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
|
||||
|
||||
DEPENDENCIES = ['abode']
|
||||
DEPENDENCIES = ["abode"]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -23,9 +26,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||
|
||||
data = hass.data[ABODE_DOMAIN]
|
||||
|
||||
device_types = [CONST.TYPE_CONNECTIVITY, CONST.TYPE_MOISTURE,
|
||||
CONST.TYPE_MOTION, CONST.TYPE_OCCUPANCY,
|
||||
CONST.TYPE_OPENING]
|
||||
device_types = [
|
||||
CONST.TYPE_CONNECTIVITY,
|
||||
CONST.TYPE_MOISTURE,
|
||||
CONST.TYPE_MOTION,
|
||||
CONST.TYPE_OCCUPANCY,
|
||||
CONST.TYPE_OPENING,
|
||||
]
|
||||
|
||||
devices = []
|
||||
for device in data.abode.get_devices(generic_type=device_types):
|
||||
|
|
@ -34,13 +41,15 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||
|
||||
devices.append(AbodeBinarySensor(data, device))
|
||||
|
||||
for automation in data.abode.get_automations(
|
||||
generic_type=CONST.TYPE_QUICK_ACTION):
|
||||
for automation in data.abode.get_automations(generic_type=CONST.TYPE_QUICK_ACTION):
|
||||
if data.is_automation_excluded(automation):
|
||||
continue
|
||||
|
||||
devices.append(AbodeQuickActionBinarySensor(
|
||||
data, automation, TIMELINE.AUTOMATION_EDIT_GROUP))
|
||||
devices.append(
|
||||
AbodeQuickActionBinarySensor(
|
||||
data, automation, TIMELINE.AUTOMATION_EDIT_GROUP
|
||||
)
|
||||
)
|
||||
|
||||
data.devices.extend(devices)
|
||||
|
||||
|
|
|
|||
|
|
@ -11,20 +11,25 @@ import voluptuous as vol
|
|||
|
||||
from homeassistant.components.ads import CONF_ADS_VAR, DATA_ADS
|
||||
from homeassistant.components.binary_sensor import (
|
||||
DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA, BinarySensorDevice)
|
||||
DEVICE_CLASSES_SCHEMA,
|
||||
PLATFORM_SCHEMA,
|
||||
BinarySensorDevice,
|
||||
)
|
||||
from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = 'ADS binary sensor'
|
||||
DEPENDENCIES = ['ads']
|
||||
DEFAULT_NAME = "ADS binary sensor"
|
||||
DEPENDENCIES = ["ads"]
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_ADS_VAR): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
|
|
@ -46,22 +51,26 @@ class AdsBinarySensor(BinarySensorDevice):
|
|||
"""Initialize ADS binary sensor."""
|
||||
self._name = name
|
||||
self._state = False
|
||||
self._device_class = device_class or 'moving'
|
||||
self._device_class = device_class or "moving"
|
||||
self._ads_hub = ads_hub
|
||||
self.ads_var = ads_var
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Register device notification."""
|
||||
|
||||
def update(name, value):
|
||||
"""Handle device notifications."""
|
||||
_LOGGER.debug('Variable %s changed its value to %d', name, value)
|
||||
_LOGGER.debug("Variable %s changed its value to %d", name, value)
|
||||
self._state = value
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
self.hass.async_add_job(
|
||||
self._ads_hub.add_device_notification,
|
||||
self.ads_var, self._ads_hub.PLCTYPE_BOOL, update)
|
||||
self.ads_var,
|
||||
self._ads_hub.PLCTYPE_BOOL,
|
||||
update,
|
||||
)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
|
|
|||
|
|
@ -9,23 +9,31 @@ import logging
|
|||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.alarmdecoder import (
|
||||
ZONE_SCHEMA, CONF_ZONES, CONF_ZONE_NAME, CONF_ZONE_TYPE,
|
||||
CONF_ZONE_RFID, SIGNAL_ZONE_FAULT, SIGNAL_ZONE_RESTORE,
|
||||
SIGNAL_RFX_MESSAGE, SIGNAL_REL_MESSAGE, CONF_RELAY_ADDR,
|
||||
CONF_RELAY_CHAN)
|
||||
ZONE_SCHEMA,
|
||||
CONF_ZONES,
|
||||
CONF_ZONE_NAME,
|
||||
CONF_ZONE_TYPE,
|
||||
CONF_ZONE_RFID,
|
||||
SIGNAL_ZONE_FAULT,
|
||||
SIGNAL_ZONE_RESTORE,
|
||||
SIGNAL_RFX_MESSAGE,
|
||||
SIGNAL_REL_MESSAGE,
|
||||
CONF_RELAY_ADDR,
|
||||
CONF_RELAY_CHAN,
|
||||
)
|
||||
|
||||
DEPENDENCIES = ['alarmdecoder']
|
||||
DEPENDENCIES = ["alarmdecoder"]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_RF_BIT0 = 'rf_bit0'
|
||||
ATTR_RF_LOW_BAT = 'rf_low_battery'
|
||||
ATTR_RF_SUPERVISED = 'rf_supervised'
|
||||
ATTR_RF_BIT3 = 'rf_bit3'
|
||||
ATTR_RF_LOOP3 = 'rf_loop3'
|
||||
ATTR_RF_LOOP2 = 'rf_loop2'
|
||||
ATTR_RF_LOOP4 = 'rf_loop4'
|
||||
ATTR_RF_LOOP1 = 'rf_loop1'
|
||||
ATTR_RF_BIT0 = "rf_bit0"
|
||||
ATTR_RF_LOW_BAT = "rf_low_battery"
|
||||
ATTR_RF_SUPERVISED = "rf_supervised"
|
||||
ATTR_RF_BIT3 = "rf_bit3"
|
||||
ATTR_RF_LOOP3 = "rf_loop3"
|
||||
ATTR_RF_LOOP2 = "rf_loop2"
|
||||
ATTR_RF_LOOP4 = "rf_loop4"
|
||||
ATTR_RF_LOOP1 = "rf_loop1"
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
|
|
@ -41,7 +49,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||
relay_addr = device_config_data.get(CONF_RELAY_ADDR)
|
||||
relay_chan = device_config_data.get(CONF_RELAY_CHAN)
|
||||
device = AlarmDecoderBinarySensor(
|
||||
zone_num, zone_name, zone_type, zone_rfid, relay_addr, relay_chan)
|
||||
zone_num, zone_name, zone_type, zone_rfid, relay_addr, relay_chan
|
||||
)
|
||||
devices.append(device)
|
||||
|
||||
add_entities(devices)
|
||||
|
|
@ -52,8 +61,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||
class AlarmDecoderBinarySensor(BinarySensorDevice):
|
||||
"""Representation of an AlarmDecoder binary sensor."""
|
||||
|
||||
def __init__(self, zone_number, zone_name, zone_type, zone_rfid,
|
||||
relay_addr, relay_chan):
|
||||
def __init__(
|
||||
self, zone_number, zone_name, zone_type, zone_rfid, relay_addr, relay_chan
|
||||
):
|
||||
"""Initialize the binary_sensor."""
|
||||
self._zone_number = zone_number
|
||||
self._zone_type = zone_type
|
||||
|
|
@ -68,16 +78,20 @@ class AlarmDecoderBinarySensor(BinarySensorDevice):
|
|||
def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
self.hass.helpers.dispatcher.async_dispatcher_connect(
|
||||
SIGNAL_ZONE_FAULT, self._fault_callback)
|
||||
SIGNAL_ZONE_FAULT, self._fault_callback
|
||||
)
|
||||
|
||||
self.hass.helpers.dispatcher.async_dispatcher_connect(
|
||||
SIGNAL_ZONE_RESTORE, self._restore_callback)
|
||||
SIGNAL_ZONE_RESTORE, self._restore_callback
|
||||
)
|
||||
|
||||
self.hass.helpers.dispatcher.async_dispatcher_connect(
|
||||
SIGNAL_RFX_MESSAGE, self._rfx_message_callback)
|
||||
SIGNAL_RFX_MESSAGE, self._rfx_message_callback
|
||||
)
|
||||
|
||||
self.hass.helpers.dispatcher.async_dispatcher_connect(
|
||||
SIGNAL_REL_MESSAGE, self._rel_message_callback)
|
||||
SIGNAL_REL_MESSAGE, self._rel_message_callback
|
||||
)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
|
@ -134,9 +148,9 @@ class AlarmDecoderBinarySensor(BinarySensorDevice):
|
|||
|
||||
def _rel_message_callback(self, message):
|
||||
"""Update relay state."""
|
||||
if (self._relay_addr == message.address and
|
||||
self._relay_chan == message.channel):
|
||||
_LOGGER.debug("Relay %d:%d value:%d", message.address,
|
||||
message.channel, message.value)
|
||||
if self._relay_addr == message.address and self._relay_chan == message.channel:
|
||||
_LOGGER.debug(
|
||||
"Relay %d:%d value:%d", message.address, message.channel, message.value
|
||||
)
|
||||
self._state = message.value
|
||||
self.schedule_update_ha_state()
|
||||
|
|
|
|||
|
|
@ -8,14 +8,18 @@ import asyncio
|
|||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.android_ip_webcam import (
|
||||
KEY_MAP, DATA_IP_WEBCAM, AndroidIPCamEntity, CONF_HOST, CONF_NAME)
|
||||
KEY_MAP,
|
||||
DATA_IP_WEBCAM,
|
||||
AndroidIPCamEntity,
|
||||
CONF_HOST,
|
||||
CONF_NAME,
|
||||
)
|
||||
|
||||
DEPENDENCIES = ['android_ip_webcam']
|
||||
DEPENDENCIES = ["android_ip_webcam"]
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||
"""Set up the IP Webcam binary sensors."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
|
@ -24,8 +28,7 @@ def async_setup_platform(hass, config, async_add_entities,
|
|||
name = discovery_info[CONF_NAME]
|
||||
ipcam = hass.data[DATA_IP_WEBCAM][host]
|
||||
|
||||
async_add_entities(
|
||||
[IPWebcamBinarySensor(name, host, ipcam, 'motion_active')], True)
|
||||
async_add_entities([IPWebcamBinarySensor(name, host, ipcam, "motion_active")], True)
|
||||
|
||||
|
||||
class IPWebcamBinarySensor(AndroidIPCamEntity, BinarySensorDevice):
|
||||
|
|
@ -37,7 +40,7 @@ class IPWebcamBinarySensor(AndroidIPCamEntity, BinarySensorDevice):
|
|||
|
||||
self._sensor = sensor
|
||||
self._mapped_name = KEY_MAP.get(self._sensor, self._sensor)
|
||||
self._name = '{} {}'.format(name, self._mapped_name)
|
||||
self._name = "{} {}".format(name, self._mapped_name)
|
||||
self._state = None
|
||||
self._unit = None
|
||||
|
||||
|
|
@ -60,4 +63,4 @@ class IPWebcamBinarySensor(AndroidIPCamEntity, BinarySensorDevice):
|
|||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this device, from component DEVICE_CLASSES."""
|
||||
return 'motion'
|
||||
return "motion"
|
||||
|
|
|
|||
|
|
@ -6,18 +6,17 @@ https://home-assistant.io/components/binary_sensor.apcupsd/
|
|||
"""
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA
|
||||
from homeassistant.const import CONF_NAME
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components import apcupsd
|
||||
|
||||
DEFAULT_NAME = 'UPS Online Status'
|
||||
DEFAULT_NAME = "UPS Online Status"
|
||||
DEPENDENCIES = [apcupsd.DOMAIN]
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
})
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string}
|
||||
)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
|
|
|
|||
|
|
@ -11,9 +11,11 @@ import requests
|
|||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA, DEVICE_CLASSES_SCHEMA)
|
||||
from homeassistant.const import (
|
||||
CONF_RESOURCE, CONF_PIN, CONF_NAME, CONF_DEVICE_CLASS)
|
||||
BinarySensorDevice,
|
||||
PLATFORM_SCHEMA,
|
||||
DEVICE_CLASSES_SCHEMA,
|
||||
)
|
||||
from homeassistant.const import CONF_RESOURCE, CONF_PIN, CONF_NAME, CONF_DEVICE_CLASS
|
||||
from homeassistant.util import Throttle
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
|
|
@ -21,12 +23,14 @@ _LOGGER = logging.getLogger(__name__)
|
|||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_RESOURCE): cv.url,
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Required(CONF_PIN): cv.string,
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
|
|
@ -38,8 +42,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||
try:
|
||||
response = requests.get(resource, timeout=10).json()
|
||||
except requests.exceptions.MissingSchema:
|
||||
_LOGGER.error("Missing resource or schema in configuration. "
|
||||
"Add http:// to your URL")
|
||||
_LOGGER.error(
|
||||
"Missing resource or schema in configuration. " "Add http:// to your URL"
|
||||
)
|
||||
return False
|
||||
except requests.exceptions.ConnectionError:
|
||||
_LOGGER.error("No route to device at %s", resource)
|
||||
|
|
@ -47,9 +52,18 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||
|
||||
arest = ArestData(resource, pin)
|
||||
|
||||
add_entities([ArestBinarySensor(
|
||||
arest, resource, config.get(CONF_NAME, response[CONF_NAME]),
|
||||
device_class, pin)], True)
|
||||
add_entities(
|
||||
[
|
||||
ArestBinarySensor(
|
||||
arest,
|
||||
resource,
|
||||
config.get(CONF_NAME, response[CONF_NAME]),
|
||||
device_class,
|
||||
pin,
|
||||
)
|
||||
],
|
||||
True,
|
||||
)
|
||||
|
||||
|
||||
class ArestBinarySensor(BinarySensorDevice):
|
||||
|
|
@ -65,7 +79,8 @@ class ArestBinarySensor(BinarySensorDevice):
|
|||
|
||||
if self._pin is not None:
|
||||
request = requests.get(
|
||||
'{}/mode/{}/i'.format(self._resource, self._pin), timeout=10)
|
||||
"{}/mode/{}/i".format(self._resource, self._pin), timeout=10
|
||||
)
|
||||
if request.status_code != 200:
|
||||
_LOGGER.error("Can't set mode of %s", self._resource)
|
||||
|
||||
|
|
@ -77,7 +92,7 @@ class ArestBinarySensor(BinarySensorDevice):
|
|||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the binary sensor is on."""
|
||||
return bool(self.arest.data.get('state'))
|
||||
return bool(self.arest.data.get("state"))
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
|
|
@ -102,8 +117,9 @@ class ArestData:
|
|||
def update(self):
|
||||
"""Get the latest data from aREST device."""
|
||||
try:
|
||||
response = requests.get('{}/digital/{}'.format(
|
||||
self._resource, self._pin), timeout=10)
|
||||
self.data = {'state': response.json()['return_value']}
|
||||
response = requests.get(
|
||||
"{}/digital/{}".format(self._resource, self._pin), timeout=10
|
||||
)
|
||||
self.data = {"state": response.json()["return_value"]}
|
||||
except requests.exceptions.ConnectionError:
|
||||
_LOGGER.error("No route to device '%s'", self._resource)
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@ https://home-assistant.io/components/sensor.august/
|
|||
from datetime import timedelta, datetime
|
||||
|
||||
from homeassistant.components.august import DATA_AUGUST
|
||||
from homeassistant.components.binary_sensor import (BinarySensorDevice)
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
|
||||
DEPENDENCIES = ['august']
|
||||
DEPENDENCIES = ["august"]
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=5)
|
||||
|
||||
|
|
@ -22,21 +22,21 @@ def _retrieve_online_state(data, doorbell):
|
|||
|
||||
def _retrieve_motion_state(data, doorbell):
|
||||
from august.activity import ActivityType
|
||||
return _activity_time_based_state(data, doorbell,
|
||||
[ActivityType.DOORBELL_MOTION,
|
||||
ActivityType.DOORBELL_DING])
|
||||
|
||||
return _activity_time_based_state(
|
||||
data, doorbell, [ActivityType.DOORBELL_MOTION, ActivityType.DOORBELL_DING]
|
||||
)
|
||||
|
||||
|
||||
def _retrieve_ding_state(data, doorbell):
|
||||
from august.activity import ActivityType
|
||||
return _activity_time_based_state(data, doorbell,
|
||||
[ActivityType.DOORBELL_DING])
|
||||
|
||||
return _activity_time_based_state(data, doorbell, [ActivityType.DOORBELL_DING])
|
||||
|
||||
|
||||
def _activity_time_based_state(data, doorbell, activity_types):
|
||||
"""Get the latest state of the sensor."""
|
||||
latest = data.get_latest_device_activity(doorbell.device_id,
|
||||
*activity_types)
|
||||
latest = data.get_latest_device_activity(doorbell.device_id, *activity_types)
|
||||
|
||||
if latest is not None:
|
||||
start = latest.activity_start_time
|
||||
|
|
@ -47,9 +47,9 @@ def _activity_time_based_state(data, doorbell, activity_types):
|
|||
|
||||
# Sensor types: Name, device_class, state_provider
|
||||
SENSOR_TYPES = {
|
||||
'doorbell_ding': ['Ding', 'occupancy', _retrieve_ding_state],
|
||||
'doorbell_motion': ['Motion', 'motion', _retrieve_motion_state],
|
||||
'doorbell_online': ['Online', 'connectivity', _retrieve_online_state],
|
||||
"doorbell_ding": ["Ding", "occupancy", _retrieve_ding_state],
|
||||
"doorbell_motion": ["Motion", "motion", _retrieve_motion_state],
|
||||
"doorbell_online": ["Online", "connectivity", _retrieve_online_state],
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -88,8 +88,9 @@ class AugustBinarySensor(BinarySensorDevice):
|
|||
@property
|
||||
def name(self):
|
||||
"""Return the name of the binary sensor."""
|
||||
return "{} {}".format(self._doorbell.device_name,
|
||||
SENSOR_TYPES[self._sensor_type][0])
|
||||
return "{} {}".format(
|
||||
self._doorbell.device_name, SENSOR_TYPES[self._sensor_type][0]
|
||||
)
|
||||
|
||||
def update(self):
|
||||
"""Get the latest state of the sensor."""
|
||||
|
|
|
|||
|
|
@ -11,20 +11,18 @@ from aiohttp.hdrs import USER_AGENT
|
|||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
PLATFORM_SCHEMA, BinarySensorDevice)
|
||||
from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice
|
||||
from homeassistant.const import CONF_NAME, ATTR_ATTRIBUTION
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_ATTRIBUTION = "Data provided by the National Oceanic and Atmospheric" \
|
||||
"Administration"
|
||||
CONF_THRESHOLD = 'forecast_threshold'
|
||||
CONF_ATTRIBUTION = "Data provided by the National Oceanic and Atmospheric" "Administration"
|
||||
CONF_THRESHOLD = "forecast_threshold"
|
||||
|
||||
DEFAULT_DEVICE_CLASS = 'visible'
|
||||
DEFAULT_NAME = 'Aurora Visibility'
|
||||
DEFAULT_DEVICE_CLASS = "visible"
|
||||
DEFAULT_NAME = "Aurora Visibility"
|
||||
DEFAULT_THRESHOLD = 75
|
||||
|
||||
HA_USER_AGENT = "Home Assistant Aurora Tracker v.0.1.0"
|
||||
|
|
@ -33,10 +31,12 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5)
|
|||
|
||||
URL = "http://services.swpc.noaa.gov/text/aurora-nowcast-map.txt"
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_THRESHOLD, default=DEFAULT_THRESHOLD): cv.positive_int,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
|
|
@ -49,12 +49,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||
threshold = config.get(CONF_THRESHOLD)
|
||||
|
||||
try:
|
||||
aurora_data = AuroraData(
|
||||
hass.config.latitude, hass.config.longitude, threshold)
|
||||
aurora_data = AuroraData(hass.config.latitude, hass.config.longitude, threshold)
|
||||
aurora_data.update()
|
||||
except requests.exceptions.HTTPError as error:
|
||||
_LOGGER.error(
|
||||
"Connection to aurora forecast service failed: %s", error)
|
||||
_LOGGER.error("Connection to aurora forecast service failed: %s", error)
|
||||
return False
|
||||
|
||||
add_entities([AuroraSensor(aurora_data, name)], True)
|
||||
|
|
@ -71,7 +69,7 @@ class AuroraSensor(BinarySensorDevice):
|
|||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return '{}'.format(self._name)
|
||||
return "{}".format(self._name)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
|
|
@ -89,8 +87,8 @@ class AuroraSensor(BinarySensorDevice):
|
|||
attrs = {}
|
||||
|
||||
if self.aurora_data:
|
||||
attrs['visibility_level'] = self.aurora_data.visibility_level
|
||||
attrs['message'] = self.aurora_data.is_visible_text
|
||||
attrs["visibility_level"] = self.aurora_data.visibility_level
|
||||
attrs["message"] = self.aurora_data.is_visible_text
|
||||
attrs[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION
|
||||
return attrs
|
||||
|
||||
|
|
@ -127,8 +125,7 @@ class AuroraData:
|
|||
self.is_visible_text = "nothing's out"
|
||||
|
||||
except requests.exceptions.HTTPError as error:
|
||||
_LOGGER.error(
|
||||
"Connection to aurora forecast service failed: %s", error)
|
||||
_LOGGER.error("Connection to aurora forecast service failed: %s", error)
|
||||
return False
|
||||
|
||||
def get_aurora_forecast(self):
|
||||
|
|
@ -141,9 +138,11 @@ class AuroraData:
|
|||
]
|
||||
|
||||
# Convert lat and long for data points in table
|
||||
converted_latitude = round((self.latitude / 180)
|
||||
* self.number_of_latitude_intervals)
|
||||
converted_longitude = round((self.longitude / 360)
|
||||
* self.number_of_longitude_intervals)
|
||||
converted_latitude = round(
|
||||
(self.latitude / 180) * self.number_of_latitude_intervals
|
||||
)
|
||||
converted_longitude = round(
|
||||
(self.longitude / 360) * self.number_of_longitude_intervals
|
||||
)
|
||||
|
||||
return forecast_table[converted_latitude][converted_longitude]
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ from homeassistant.const import CONF_TRIGGER_TIME
|
|||
from homeassistant.helpers.event import track_point_in_utc_time
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
DEPENDENCIES = ['axis']
|
||||
DEPENDENCIES = ["axis"]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -55,13 +55,14 @@ class AxisBinarySensor(AxisDeviceEvent, BinarySensorDevice):
|
|||
# Set timer to wait until updating the state
|
||||
def _delay_update(now):
|
||||
"""Timer callback for sensor update."""
|
||||
_LOGGER.debug("%s called delayed (%s sec) update",
|
||||
self._name, self._delay)
|
||||
_LOGGER.debug(
|
||||
"%s called delayed (%s sec) update", self._name, self._delay
|
||||
)
|
||||
self.schedule_update_ha_state()
|
||||
self._timer = None
|
||||
|
||||
self._timer = track_point_in_utc_time(
|
||||
self.hass, _delay_update,
|
||||
utcnow() + timedelta(seconds=self._delay))
|
||||
self.hass, _delay_update, utcnow() + timedelta(seconds=self._delay)
|
||||
)
|
||||
else:
|
||||
self.schedule_update_ha_state()
|
||||
|
|
|
|||
|
|
@ -11,58 +11,73 @@ from collections import OrderedDict
|
|||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA
|
||||
from homeassistant.const import (
|
||||
CONF_ABOVE, CONF_BELOW, CONF_DEVICE_CLASS, CONF_ENTITY_ID, CONF_NAME,
|
||||
CONF_PLATFORM, CONF_STATE, STATE_UNKNOWN)
|
||||
CONF_ABOVE,
|
||||
CONF_BELOW,
|
||||
CONF_DEVICE_CLASS,
|
||||
CONF_ENTITY_ID,
|
||||
CONF_NAME,
|
||||
CONF_PLATFORM,
|
||||
CONF_STATE,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import condition
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_OBSERVATIONS = 'observations'
|
||||
ATTR_PROBABILITY = 'probability'
|
||||
ATTR_PROBABILITY_THRESHOLD = 'probability_threshold'
|
||||
ATTR_OBSERVATIONS = "observations"
|
||||
ATTR_PROBABILITY = "probability"
|
||||
ATTR_PROBABILITY_THRESHOLD = "probability_threshold"
|
||||
|
||||
CONF_OBSERVATIONS = 'observations'
|
||||
CONF_PRIOR = 'prior'
|
||||
CONF_PROBABILITY_THRESHOLD = 'probability_threshold'
|
||||
CONF_P_GIVEN_F = 'prob_given_false'
|
||||
CONF_P_GIVEN_T = 'prob_given_true'
|
||||
CONF_TO_STATE = 'to_state'
|
||||
CONF_OBSERVATIONS = "observations"
|
||||
CONF_PRIOR = "prior"
|
||||
CONF_PROBABILITY_THRESHOLD = "probability_threshold"
|
||||
CONF_P_GIVEN_F = "prob_given_false"
|
||||
CONF_P_GIVEN_T = "prob_given_true"
|
||||
CONF_TO_STATE = "to_state"
|
||||
|
||||
DEFAULT_NAME = "Bayesian Binary Sensor"
|
||||
DEFAULT_PROBABILITY_THRESHOLD = 0.5
|
||||
|
||||
NUMERIC_STATE_SCHEMA = vol.Schema({
|
||||
CONF_PLATFORM: 'numeric_state',
|
||||
NUMERIC_STATE_SCHEMA = vol.Schema(
|
||||
{
|
||||
CONF_PLATFORM: "numeric_state",
|
||||
vol.Required(CONF_ENTITY_ID): cv.entity_id,
|
||||
vol.Optional(CONF_ABOVE): vol.Coerce(float),
|
||||
vol.Optional(CONF_BELOW): vol.Coerce(float),
|
||||
vol.Required(CONF_P_GIVEN_T): vol.Coerce(float),
|
||||
vol.Optional(CONF_P_GIVEN_F): vol.Coerce(float)
|
||||
}, required=True)
|
||||
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,
|
||||
vol.Required(CONF_TO_STATE): cv.string,
|
||||
vol.Required(CONF_P_GIVEN_T): vol.Coerce(float),
|
||||
vol.Optional(CONF_P_GIVEN_F): vol.Coerce(float)
|
||||
}, required=True)
|
||||
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.Required(CONF_OBSERVATIONS):
|
||||
vol.Schema(vol.All(cv.ensure_list,
|
||||
[vol.Any(NUMERIC_STATE_SCHEMA, STATE_SCHEMA)])),
|
||||
vol.Required(CONF_OBSERVATIONS): vol.Schema(
|
||||
vol.All(cv.ensure_list, [vol.Any(NUMERIC_STATE_SCHEMA, STATE_SCHEMA)])
|
||||
),
|
||||
vol.Required(CONF_PRIOR): vol.Coerce(float),
|
||||
vol.Optional(CONF_PROBABILITY_THRESHOLD,
|
||||
default=DEFAULT_PROBABILITY_THRESHOLD): vol.Coerce(float),
|
||||
})
|
||||
vol.Optional(
|
||||
CONF_PROBABILITY_THRESHOLD, default=DEFAULT_PROBABILITY_THRESHOLD
|
||||
): vol.Coerce(float),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def update_probability(prior, prob_true, prob_false):
|
||||
|
|
@ -75,8 +90,7 @@ def update_probability(prior, prob_true, prob_false):
|
|||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||
"""Set up the Bayesian Binary sensor."""
|
||||
name = config.get(CONF_NAME)
|
||||
observations = config.get(CONF_OBSERVATIONS)
|
||||
|
|
@ -84,17 +98,20 @@ def async_setup_platform(hass, config, async_add_entities,
|
|||
probability_threshold = config.get(CONF_PROBABILITY_THRESHOLD)
|
||||
device_class = config.get(CONF_DEVICE_CLASS)
|
||||
|
||||
async_add_entities([
|
||||
async_add_entities(
|
||||
[
|
||||
BayesianBinarySensor(
|
||||
name, prior, observations, probability_threshold, device_class)
|
||||
], True)
|
||||
name, prior, observations, probability_threshold, device_class
|
||||
)
|
||||
],
|
||||
True,
|
||||
)
|
||||
|
||||
|
||||
class BayesianBinarySensor(BinarySensorDevice):
|
||||
"""Representation of a Bayesian sensor."""
|
||||
|
||||
def __init__(self, name, prior, observations, probability_threshold,
|
||||
device_class):
|
||||
def __init__(self, name, prior, observations, probability_threshold, device_class):
|
||||
"""Initialize the Bayesian sensor."""
|
||||
self._name = name
|
||||
self._observations = observations
|
||||
|
|
@ -106,25 +123,25 @@ class BayesianBinarySensor(BinarySensorDevice):
|
|||
|
||||
self.current_obs = OrderedDict({})
|
||||
|
||||
to_observe = set(obs['entity_id'] for obs in self._observations)
|
||||
to_observe = set(obs["entity_id"] for obs in self._observations)
|
||||
|
||||
self.entity_obs = dict.fromkeys(to_observe, [])
|
||||
|
||||
for ind, obs in enumerate(self._observations):
|
||||
obs['id'] = ind
|
||||
self.entity_obs[obs['entity_id']].append(obs)
|
||||
obs["id"] = ind
|
||||
self.entity_obs[obs["entity_id"]].append(obs)
|
||||
|
||||
self.watchers = {
|
||||
'numeric_state': self._process_numeric_state,
|
||||
'state': self._process_state
|
||||
"numeric_state": self._process_numeric_state,
|
||||
"state": self._process_state,
|
||||
}
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Call when entity about to be added."""
|
||||
|
||||
@callback
|
||||
def async_threshold_sensor_state_listener(entity, old_state,
|
||||
new_state):
|
||||
def async_threshold_sensor_state_listener(entity, old_state, new_state):
|
||||
"""Handle sensor state changes."""
|
||||
if new_state.state == STATE_UNKNOWN:
|
||||
return
|
||||
|
|
@ -132,34 +149,33 @@ class BayesianBinarySensor(BinarySensorDevice):
|
|||
entity_obs_list = self.entity_obs[entity]
|
||||
|
||||
for entity_obs in entity_obs_list:
|
||||
platform = entity_obs['platform']
|
||||
platform = entity_obs["platform"]
|
||||
|
||||
self.watchers[platform](entity_obs)
|
||||
|
||||
prior = self.prior
|
||||
for obs in self.current_obs.values():
|
||||
prior = update_probability(
|
||||
prior, obs['prob_true'], obs['prob_false'])
|
||||
prior = update_probability(prior, obs["prob_true"], obs["prob_false"])
|
||||
self.probability = prior
|
||||
|
||||
self.hass.async_add_job(self.async_update_ha_state, True)
|
||||
|
||||
entities = [obs['entity_id'] for obs in self._observations]
|
||||
entities = [obs["entity_id"] for obs in self._observations]
|
||||
async_track_state_change(
|
||||
self.hass, entities, async_threshold_sensor_state_listener)
|
||||
self.hass, entities, async_threshold_sensor_state_listener
|
||||
)
|
||||
|
||||
def _update_current_obs(self, entity_observation, should_trigger):
|
||||
"""Update current observation."""
|
||||
obs_id = entity_observation['id']
|
||||
obs_id = entity_observation["id"]
|
||||
|
||||
if should_trigger:
|
||||
prob_true = entity_observation['prob_given_true']
|
||||
prob_false = entity_observation.get(
|
||||
'prob_given_false', 1 - prob_true)
|
||||
prob_true = entity_observation["prob_given_true"]
|
||||
prob_false = entity_observation.get("prob_given_false", 1 - prob_true)
|
||||
|
||||
self.current_obs[obs_id] = {
|
||||
'prob_true': prob_true,
|
||||
'prob_false': prob_false
|
||||
"prob_true": prob_true,
|
||||
"prob_false": prob_false,
|
||||
}
|
||||
|
||||
else:
|
||||
|
|
@ -167,21 +183,26 @@ class BayesianBinarySensor(BinarySensorDevice):
|
|||
|
||||
def _process_numeric_state(self, entity_observation):
|
||||
"""Add entity to current_obs if numeric state conditions are met."""
|
||||
entity = entity_observation['entity_id']
|
||||
entity = entity_observation["entity_id"]
|
||||
|
||||
should_trigger = condition.async_numeric_state(
|
||||
self.hass, entity,
|
||||
entity_observation.get('below'),
|
||||
entity_observation.get('above'), None, entity_observation)
|
||||
self.hass,
|
||||
entity,
|
||||
entity_observation.get("below"),
|
||||
entity_observation.get("above"),
|
||||
None,
|
||||
entity_observation,
|
||||
)
|
||||
|
||||
self._update_current_obs(entity_observation, should_trigger)
|
||||
|
||||
def _process_state(self, entity_observation):
|
||||
"""Add entity to current observations if state conditions are met."""
|
||||
entity = entity_observation['entity_id']
|
||||
entity = entity_observation["entity_id"]
|
||||
|
||||
should_trigger = condition.state(
|
||||
self.hass, entity, entity_observation.get('to_state'))
|
||||
self.hass, entity, entity_observation.get("to_state")
|
||||
)
|
||||
|
||||
self._update_current_obs(entity_observation, should_trigger)
|
||||
|
||||
|
|
|
|||
|
|
@ -9,36 +9,35 @@ import logging
|
|||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import bbb_gpio
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.const import (DEVICE_DEFAULT_NAME, CONF_NAME)
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA
|
||||
from homeassistant.const import DEVICE_DEFAULT_NAME, CONF_NAME
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['bbb_gpio']
|
||||
DEPENDENCIES = ["bbb_gpio"]
|
||||
|
||||
CONF_PINS = 'pins'
|
||||
CONF_BOUNCETIME = 'bouncetime'
|
||||
CONF_INVERT_LOGIC = 'invert_logic'
|
||||
CONF_PULL_MODE = 'pull_mode'
|
||||
CONF_PINS = "pins"
|
||||
CONF_BOUNCETIME = "bouncetime"
|
||||
CONF_INVERT_LOGIC = "invert_logic"
|
||||
CONF_PULL_MODE = "pull_mode"
|
||||
|
||||
DEFAULT_BOUNCETIME = 50
|
||||
DEFAULT_INVERT_LOGIC = False
|
||||
DEFAULT_PULL_MODE = 'UP'
|
||||
DEFAULT_PULL_MODE = "UP"
|
||||
|
||||
PIN_SCHEMA = vol.Schema({
|
||||
PIN_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_BOUNCETIME, default=DEFAULT_BOUNCETIME): cv.positive_int,
|
||||
vol.Optional(CONF_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean,
|
||||
vol.Optional(CONF_PULL_MODE, default=DEFAULT_PULL_MODE):
|
||||
vol.In(['UP', 'DOWN'])
|
||||
})
|
||||
vol.Optional(CONF_PULL_MODE, default=DEFAULT_PULL_MODE): vol.In(["UP", "DOWN"]),
|
||||
}
|
||||
)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_PINS, default={}):
|
||||
vol.Schema({cv.string: PIN_SCHEMA}),
|
||||
})
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{vol.Required(CONF_PINS, default={}): vol.Schema({cv.string: PIN_SCHEMA})}
|
||||
)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ https://home-assistant.io/components/binary_sensor.blink/
|
|||
from homeassistant.components.blink import DOMAIN
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
|
||||
DEPENDENCIES = ['blink']
|
||||
DEPENDENCIES = ["blink"]
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
|
|
@ -28,7 +28,7 @@ class BlinkCameraMotionSensor(BinarySensorDevice):
|
|||
|
||||
def __init__(self, name, data):
|
||||
"""Initialize the sensor."""
|
||||
self._name = 'blink_' + name + '_motion_enabled'
|
||||
self._name = "blink_" + name + "_motion_enabled"
|
||||
self._camera_name = name
|
||||
self.data = data
|
||||
self._state = self.data.cameras[self._camera_name].armed
|
||||
|
|
@ -54,7 +54,7 @@ class BlinkSystemSensor(BinarySensorDevice):
|
|||
|
||||
def __init__(self, data):
|
||||
"""Initialize the sensor."""
|
||||
self._name = 'blink armed status'
|
||||
self._name = "blink armed status"
|
||||
self.data = data
|
||||
self._state = self.data.arm
|
||||
|
||||
|
|
|
|||
|
|
@ -8,24 +8,23 @@ import logging
|
|||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA
|
||||
from homeassistant.const import CONF_MONITORED_CONDITIONS
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['bloomsky']
|
||||
DEPENDENCIES = ["bloomsky"]
|
||||
|
||||
SENSOR_TYPES = {
|
||||
'Rain': 'moisture',
|
||||
'Night': None,
|
||||
SENSOR_TYPES = {"Rain": "moisture", "Night": None}
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)): vol.All(
|
||||
cv.ensure_list, [vol.In(SENSOR_TYPES)]
|
||||
)
|
||||
}
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)):
|
||||
vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
|
|
@ -36,8 +35,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||
|
||||
for device in bloomsky.BLOOMSKY.devices.values():
|
||||
for variable in sensors:
|
||||
add_entities(
|
||||
[BloomSkySensor(bloomsky.BLOOMSKY, device, variable)], True)
|
||||
add_entities([BloomSkySensor(bloomsky.BLOOMSKY, device, variable)], True)
|
||||
|
||||
|
||||
class BloomSkySensor(BinarySensorDevice):
|
||||
|
|
@ -46,9 +44,9 @@ class BloomSkySensor(BinarySensorDevice):
|
|||
def __init__(self, bs, device, sensor_name):
|
||||
"""Initialize a BloomSky binary sensor."""
|
||||
self._bloomsky = bs
|
||||
self._device_id = device['DeviceID']
|
||||
self._device_id = device["DeviceID"]
|
||||
self._sensor_name = sensor_name
|
||||
self._name = '{} {}'.format(device['DeviceName'], sensor_name)
|
||||
self._name = "{} {}".format(device["DeviceName"], sensor_name)
|
||||
self._state = None
|
||||
|
||||
@property
|
||||
|
|
@ -70,5 +68,4 @@ class BloomSkySensor(BinarySensorDevice):
|
|||
"""Request an update from the BloomSky API."""
|
||||
self._bloomsky.refresh_devices()
|
||||
|
||||
self._state = \
|
||||
self._bloomsky.devices[self._device_id]['Data'][self._sensor_name]
|
||||
self._state = self._bloomsky.devices[self._device_id]["Data"][self._sensor_name]
|
||||
|
|
|
|||
|
|
@ -10,22 +10,22 @@ import logging
|
|||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN
|
||||
|
||||
DEPENDENCIES = ['bmw_connected_drive']
|
||||
DEPENDENCIES = ["bmw_connected_drive"]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SENSOR_TYPES = {
|
||||
'lids': ['Doors', 'opening'],
|
||||
'windows': ['Windows', 'opening'],
|
||||
'door_lock_state': ['Door lock state', 'safety'],
|
||||
'lights_parking': ['Parking lights', 'light'],
|
||||
'condition_based_services': ['Condition based services', 'problem'],
|
||||
'check_control_messages': ['Control messages', 'problem']
|
||||
"lids": ["Doors", "opening"],
|
||||
"windows": ["Windows", "opening"],
|
||||
"door_lock_state": ["Door lock state", "safety"],
|
||||
"lights_parking": ["Parking lights", "light"],
|
||||
"condition_based_services": ["Condition based services", "problem"],
|
||||
"check_control_messages": ["Control messages", "problem"],
|
||||
}
|
||||
|
||||
SENSOR_TYPES_ELEC = {
|
||||
'charging_status': ['Charging status', 'power'],
|
||||
'connection_status': ['Connection status', 'plug']
|
||||
"charging_status": ["Charging status", "power"],
|
||||
"connection_status": ["Connection status", "plug"],
|
||||
}
|
||||
|
||||
SENSOR_TYPES_ELEC.update(SENSOR_TYPES)
|
||||
|
|
@ -34,22 +34,23 @@ SENSOR_TYPES_ELEC.update(SENSOR_TYPES)
|
|||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the BMW sensors."""
|
||||
accounts = hass.data[BMW_DOMAIN]
|
||||
_LOGGER.debug('Found BMW accounts: %s',
|
||||
', '.join([a.name for a in accounts]))
|
||||
_LOGGER.debug("Found BMW accounts: %s", ", ".join([a.name for a in accounts]))
|
||||
devices = []
|
||||
for account in accounts:
|
||||
for vehicle in account.account.vehicles:
|
||||
if vehicle.has_hv_battery:
|
||||
_LOGGER.debug('BMW with a high voltage battery')
|
||||
_LOGGER.debug("BMW with a high voltage battery")
|
||||
for key, value in sorted(SENSOR_TYPES_ELEC.items()):
|
||||
device = BMWConnectedDriveSensor(account, vehicle, key,
|
||||
value[0], value[1])
|
||||
device = BMWConnectedDriveSensor(
|
||||
account, vehicle, key, value[0], value[1]
|
||||
)
|
||||
devices.append(device)
|
||||
elif vehicle.has_internal_combustion_engine:
|
||||
_LOGGER.debug('BMW with an internal combustion engine')
|
||||
_LOGGER.debug("BMW with an internal combustion engine")
|
||||
for key, value in sorted(SENSOR_TYPES.items()):
|
||||
device = BMWConnectedDriveSensor(account, vehicle, key,
|
||||
value[0], value[1])
|
||||
device = BMWConnectedDriveSensor(
|
||||
account, vehicle, key, value[0], value[1]
|
||||
)
|
||||
devices.append(device)
|
||||
add_entities(devices, True)
|
||||
|
||||
|
|
@ -57,14 +58,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||
class BMWConnectedDriveSensor(BinarySensorDevice):
|
||||
"""Representation of a BMW vehicle binary sensor."""
|
||||
|
||||
def __init__(self, account, vehicle, attribute: str, sensor_name,
|
||||
device_class):
|
||||
def __init__(self, account, vehicle, attribute: str, sensor_name, device_class):
|
||||
"""Constructor."""
|
||||
self._account = account
|
||||
self._vehicle = vehicle
|
||||
self._attribute = attribute
|
||||
self._name = '{} {}'.format(self._vehicle.name, self._attribute)
|
||||
self._unique_id = '{}-{}'.format(self._vehicle.vin, self._attribute)
|
||||
self._name = "{} {}".format(self._vehicle.name, self._attribute)
|
||||
self._unique_id = "{}-{}".format(self._vehicle.vin, self._attribute)
|
||||
self._sensor_name = sensor_name
|
||||
self._device_class = device_class
|
||||
self._state = None
|
||||
|
|
@ -101,39 +101,37 @@ class BMWConnectedDriveSensor(BinarySensorDevice):
|
|||
def device_state_attributes(self):
|
||||
"""Return the state attributes of the binary sensor."""
|
||||
vehicle_state = self._vehicle.state
|
||||
result = {
|
||||
'car': self._vehicle.name
|
||||
}
|
||||
result = {"car": self._vehicle.name}
|
||||
|
||||
if self._attribute == 'lids':
|
||||
if self._attribute == "lids":
|
||||
for lid in vehicle_state.lids:
|
||||
result[lid.name] = lid.state.value
|
||||
elif self._attribute == 'windows':
|
||||
elif self._attribute == "windows":
|
||||
for window in vehicle_state.windows:
|
||||
result[window.name] = window.state.value
|
||||
elif self._attribute == 'door_lock_state':
|
||||
result['door_lock_state'] = vehicle_state.door_lock_state.value
|
||||
result['last_update_reason'] = vehicle_state.last_update_reason
|
||||
elif self._attribute == 'lights_parking':
|
||||
result['lights_parking'] = vehicle_state.parking_lights.value
|
||||
elif self._attribute == 'condition_based_services':
|
||||
elif self._attribute == "door_lock_state":
|
||||
result["door_lock_state"] = vehicle_state.door_lock_state.value
|
||||
result["last_update_reason"] = vehicle_state.last_update_reason
|
||||
elif self._attribute == "lights_parking":
|
||||
result["lights_parking"] = vehicle_state.parking_lights.value
|
||||
elif self._attribute == "condition_based_services":
|
||||
for report in vehicle_state.condition_based_services:
|
||||
result.update(self._format_cbs_report(report))
|
||||
elif self._attribute == 'check_control_messages':
|
||||
elif self._attribute == "check_control_messages":
|
||||
check_control_messages = vehicle_state.check_control_messages
|
||||
if not check_control_messages:
|
||||
result['check_control_messages'] = 'OK'
|
||||
result["check_control_messages"] = "OK"
|
||||
else:
|
||||
result['check_control_messages'] = check_control_messages
|
||||
elif self._attribute == 'charging_status':
|
||||
result['charging_status'] = vehicle_state.charging_status.value
|
||||
result["check_control_messages"] = check_control_messages
|
||||
elif self._attribute == "charging_status":
|
||||
result["charging_status"] = vehicle_state.charging_status.value
|
||||
# pylint: disable=protected-access
|
||||
result['last_charging_end_result'] = \
|
||||
vehicle_state._attributes['lastChargingEndResult']
|
||||
if self._attribute == 'connection_status':
|
||||
result["last_charging_end_result"] = vehicle_state._attributes[
|
||||
"lastChargingEndResult"
|
||||
]
|
||||
if self._attribute == "connection_status":
|
||||
# pylint: disable=protected-access
|
||||
result['connection_status'] = \
|
||||
vehicle_state._attributes['connectionStatus']
|
||||
result["connection_status"] = vehicle_state._attributes["connectionStatus"]
|
||||
|
||||
return sorted(result.items())
|
||||
|
||||
|
|
@ -141,49 +139,52 @@ class BMWConnectedDriveSensor(BinarySensorDevice):
|
|||
"""Read new state data from the library."""
|
||||
from bimmer_connected.state import LockState
|
||||
from bimmer_connected.state import ChargingState
|
||||
|
||||
vehicle_state = self._vehicle.state
|
||||
|
||||
# device class opening: On means open, Off means closed
|
||||
if self._attribute == 'lids':
|
||||
if self._attribute == "lids":
|
||||
_LOGGER.debug("Status of lid: %s", vehicle_state.all_lids_closed)
|
||||
self._state = not vehicle_state.all_lids_closed
|
||||
if self._attribute == 'windows':
|
||||
if self._attribute == "windows":
|
||||
self._state = not vehicle_state.all_windows_closed
|
||||
# device class safety: On means unsafe, Off means safe
|
||||
if self._attribute == 'door_lock_state':
|
||||
if self._attribute == "door_lock_state":
|
||||
# Possible values: LOCKED, SECURED, SELECTIVE_LOCKED, UNLOCKED
|
||||
self._state = vehicle_state.door_lock_state not in \
|
||||
[LockState.LOCKED, LockState.SECURED]
|
||||
self._state = vehicle_state.door_lock_state not in [
|
||||
LockState.LOCKED,
|
||||
LockState.SECURED,
|
||||
]
|
||||
# device class light: On means light detected, Off means no light
|
||||
if self._attribute == 'lights_parking':
|
||||
if self._attribute == "lights_parking":
|
||||
self._state = vehicle_state.are_parking_lights_on
|
||||
# device class problem: On means problem detected, Off means no problem
|
||||
if self._attribute == 'condition_based_services':
|
||||
if self._attribute == "condition_based_services":
|
||||
self._state = not vehicle_state.are_all_cbs_ok
|
||||
if self._attribute == 'check_control_messages':
|
||||
if self._attribute == "check_control_messages":
|
||||
self._state = vehicle_state.has_check_control_messages
|
||||
# device class power: On means power detected, Off means no power
|
||||
if self._attribute == 'charging_status':
|
||||
self._state = vehicle_state.charging_status in \
|
||||
[ChargingState.CHARGING]
|
||||
if self._attribute == "charging_status":
|
||||
self._state = vehicle_state.charging_status in [ChargingState.CHARGING]
|
||||
# device class plug: On means device is plugged in,
|
||||
# Off means device is unplugged
|
||||
if self._attribute == 'connection_status':
|
||||
if self._attribute == "connection_status":
|
||||
# pylint: disable=protected-access
|
||||
self._state = (vehicle_state._attributes['connectionStatus'] ==
|
||||
'CONNECTED')
|
||||
self._state = vehicle_state._attributes["connectionStatus"] == "CONNECTED"
|
||||
|
||||
@staticmethod
|
||||
def _format_cbs_report(report):
|
||||
result = {}
|
||||
service_type = report.service_type.lower().replace('_', ' ')
|
||||
result['{} status'.format(service_type)] = report.state.value
|
||||
service_type = report.service_type.lower().replace("_", " ")
|
||||
result["{} status".format(service_type)] = report.state.value
|
||||
if report.due_date is not None:
|
||||
result['{} date'.format(service_type)] = \
|
||||
report.due_date.strftime('%Y-%m-%d')
|
||||
result["{} date".format(service_type)] = report.due_date.strftime(
|
||||
"%Y-%m-%d"
|
||||
)
|
||||
if report.due_distance is not None:
|
||||
result['{} distance'.format(service_type)] = \
|
||||
'{} km'.format(report.due_distance)
|
||||
result["{} distance".format(service_type)] = "{} km".format(
|
||||
report.due_distance
|
||||
)
|
||||
return result
|
||||
|
||||
def update_callback(self):
|
||||
|
|
|
|||
|
|
@ -11,33 +11,42 @@ import voluptuous as vol
|
|||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA)
|
||||
BinarySensorDevice,
|
||||
DEVICE_CLASSES_SCHEMA,
|
||||
PLATFORM_SCHEMA,
|
||||
)
|
||||
from homeassistant.components.sensor.command_line import CommandSensorData
|
||||
from homeassistant.const import (
|
||||
CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON, CONF_NAME, CONF_VALUE_TEMPLATE,
|
||||
CONF_COMMAND, CONF_DEVICE_CLASS)
|
||||
CONF_PAYLOAD_OFF,
|
||||
CONF_PAYLOAD_ON,
|
||||
CONF_NAME,
|
||||
CONF_VALUE_TEMPLATE,
|
||||
CONF_COMMAND,
|
||||
CONF_DEVICE_CLASS,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = 'Binary Command Sensor'
|
||||
DEFAULT_PAYLOAD_ON = 'ON'
|
||||
DEFAULT_PAYLOAD_OFF = 'OFF'
|
||||
DEFAULT_NAME = "Binary Command Sensor"
|
||||
DEFAULT_PAYLOAD_ON = "ON"
|
||||
DEFAULT_PAYLOAD_OFF = "OFF"
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=60)
|
||||
|
||||
CONF_COMMAND_TIMEOUT = 'command_timeout'
|
||||
CONF_COMMAND_TIMEOUT = "command_timeout"
|
||||
DEFAULT_TIMEOUT = 15
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_COMMAND): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string,
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
|
||||
vol.Optional(
|
||||
CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
|
||||
})
|
||||
vol.Optional(CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
|
|
@ -53,16 +62,22 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||
value_template.hass = hass
|
||||
data = CommandSensorData(hass, command, command_timeout)
|
||||
|
||||
add_entities([CommandBinarySensor(
|
||||
hass, data, name, device_class, payload_on, payload_off,
|
||||
value_template)], True)
|
||||
add_entities(
|
||||
[
|
||||
CommandBinarySensor(
|
||||
hass, data, name, device_class, payload_on, payload_off, value_template
|
||||
)
|
||||
],
|
||||
True,
|
||||
)
|
||||
|
||||
|
||||
class CommandBinarySensor(BinarySensorDevice):
|
||||
"""Representation of a command line binary sensor."""
|
||||
|
||||
def __init__(self, hass, data, name, device_class, payload_on,
|
||||
payload_off, value_template):
|
||||
def __init__(
|
||||
self, hass, data, name, device_class, payload_on, payload_off, value_template
|
||||
):
|
||||
"""Initialize the Command line binary sensor."""
|
||||
self._hass = hass
|
||||
self.data = data
|
||||
|
|
@ -94,8 +109,7 @@ class CommandBinarySensor(BinarySensorDevice):
|
|||
value = self.data.value
|
||||
|
||||
if self._value_template is not None:
|
||||
value = self._value_template.render_with_possible_json_value(
|
||||
value, False)
|
||||
value = self._value_template.render_with_possible_json_value(value, False)
|
||||
if value == self._payload_on:
|
||||
self._state = True
|
||||
elif value == self._payload_off:
|
||||
|
|
|
|||
|
|
@ -11,35 +11,39 @@ import requests
|
|||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA, DEVICE_CLASSES)
|
||||
from homeassistant.const import (CONF_HOST, CONF_PORT)
|
||||
BinarySensorDevice,
|
||||
PLATFORM_SCHEMA,
|
||||
DEVICE_CLASSES,
|
||||
)
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['concord232==0.15']
|
||||
REQUIREMENTS = ["concord232==0.15"]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_EXCLUDE_ZONES = 'exclude_zones'
|
||||
CONF_ZONE_TYPES = 'zone_types'
|
||||
CONF_EXCLUDE_ZONES = "exclude_zones"
|
||||
CONF_ZONE_TYPES = "zone_types"
|
||||
|
||||
DEFAULT_HOST = 'localhost'
|
||||
DEFAULT_NAME = 'Alarm'
|
||||
DEFAULT_PORT = '5007'
|
||||
DEFAULT_HOST = "localhost"
|
||||
DEFAULT_NAME = "Alarm"
|
||||
DEFAULT_PORT = "5007"
|
||||
DEFAULT_SSL = False
|
||||
|
||||
SCAN_INTERVAL = datetime.timedelta(seconds=10)
|
||||
|
||||
ZONE_TYPES_SCHEMA = vol.Schema({
|
||||
cv.positive_int: vol.In(DEVICE_CLASSES),
|
||||
})
|
||||
ZONE_TYPES_SCHEMA = vol.Schema({cv.positive_int: vol.In(DEVICE_CLASSES)})
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_EXCLUDE_ZONES, default=[]):
|
||||
vol.All(cv.ensure_list, [cv.positive_int]),
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_EXCLUDE_ZONES, default=[]): vol.All(
|
||||
cv.ensure_list, [cv.positive_int]
|
||||
),
|
||||
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_ZONE_TYPES, default={}): ZONE_TYPES_SCHEMA,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
|
|
@ -54,7 +58,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||
|
||||
try:
|
||||
_LOGGER.debug("Initializing client")
|
||||
client = concord232_client.Client('http://{}:{}'.format(host, port))
|
||||
client = concord232_client.Client("http://{}:{}".format(host, port))
|
||||
client.zones = client.list_zones()
|
||||
client.last_zone_update = datetime.datetime.now()
|
||||
|
||||
|
|
@ -67,15 +71,17 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||
# name mapping to different sensors in an unpredictable way. Sort
|
||||
# the zones by zone number to prevent this.
|
||||
|
||||
client.zones.sort(key=lambda zone: zone['number'])
|
||||
client.zones.sort(key=lambda zone: zone["number"])
|
||||
|
||||
for zone in client.zones:
|
||||
_LOGGER.info("Loading Zone found: %s", zone['name'])
|
||||
if zone['number'] not in exclude:
|
||||
_LOGGER.info("Loading Zone found: %s", zone["name"])
|
||||
if zone["number"] not in exclude:
|
||||
sensors.append(
|
||||
Concord232ZoneSensor(
|
||||
hass, client, zone, zone_types.get(
|
||||
zone['number'], get_opening_type(zone))
|
||||
hass,
|
||||
client,
|
||||
zone,
|
||||
zone_types.get(zone["number"], get_opening_type(zone)),
|
||||
)
|
||||
)
|
||||
|
||||
|
|
@ -84,15 +90,15 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||
|
||||
def get_opening_type(zone):
|
||||
"""Return the result of the type guessing from name."""
|
||||
if 'MOTION' in zone['name']:
|
||||
return 'motion'
|
||||
if 'KEY' in zone['name']:
|
||||
return 'safety'
|
||||
if 'SMOKE' in zone['name']:
|
||||
return 'smoke'
|
||||
if 'WATER' in zone['name']:
|
||||
return 'water'
|
||||
return 'opening'
|
||||
if "MOTION" in zone["name"]:
|
||||
return "motion"
|
||||
if "KEY" in zone["name"]:
|
||||
return "safety"
|
||||
if "SMOKE" in zone["name"]:
|
||||
return "smoke"
|
||||
if "WATER" in zone["name"]:
|
||||
return "water"
|
||||
return "opening"
|
||||
|
||||
|
||||
class Concord232ZoneSensor(BinarySensorDevice):
|
||||
|
|
@ -103,7 +109,7 @@ class Concord232ZoneSensor(BinarySensorDevice):
|
|||
self._hass = hass
|
||||
self._client = client
|
||||
self._zone = zone
|
||||
self._number = zone['number']
|
||||
self._number = zone["number"]
|
||||
self._zone_type = zone_type
|
||||
|
||||
@property
|
||||
|
|
@ -119,13 +125,13 @@ class Concord232ZoneSensor(BinarySensorDevice):
|
|||
@property
|
||||
def name(self):
|
||||
"""Return the name of the binary sensor."""
|
||||
return self._zone['name']
|
||||
return self._zone["name"]
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the binary sensor is on."""
|
||||
# True means "faulted" or "open" or "abnormal state"
|
||||
return bool(self._zone['state'] != 'Normal')
|
||||
return bool(self._zone["state"] != "Normal")
|
||||
|
||||
def update(self):
|
||||
"""Get updated stats from API."""
|
||||
|
|
@ -134,8 +140,9 @@ class Concord232ZoneSensor(BinarySensorDevice):
|
|||
if last_update > datetime.timedelta(seconds=1):
|
||||
self._client.zones = self._client.list_zones()
|
||||
self._client.last_zone_update = datetime.datetime.now()
|
||||
_LOGGER.debug("Updated from zone: %s", self._zone['name'])
|
||||
_LOGGER.debug("Updated from zone: %s", self._zone["name"])
|
||||
|
||||
if hasattr(self._client, 'zones'):
|
||||
self._zone = next((x for x in self._client.zones
|
||||
if x['number'] == self._number), None)
|
||||
if hasattr(self._client, "zones"):
|
||||
self._zone = next(
|
||||
(x for x in self._client.zones if x["number"] == self._number), None
|
||||
)
|
||||
|
|
|
|||
|
|
@ -6,38 +6,47 @@ https://home-assistant.io/components/binary_sensor.deconz/
|
|||
"""
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.deconz.const import (
|
||||
ATTR_DARK, ATTR_ON, CONF_ALLOW_CLIP_SENSOR, DOMAIN as DATA_DECONZ,
|
||||
DATA_DECONZ_ID, DATA_DECONZ_UNSUB, DECONZ_DOMAIN)
|
||||
ATTR_DARK,
|
||||
ATTR_ON,
|
||||
CONF_ALLOW_CLIP_SENSOR,
|
||||
DOMAIN as DATA_DECONZ,
|
||||
DATA_DECONZ_ID,
|
||||
DATA_DECONZ_UNSUB,
|
||||
DECONZ_DOMAIN,
|
||||
)
|
||||
from homeassistant.const import ATTR_BATTERY_LEVEL
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
DEPENDENCIES = ['deconz']
|
||||
DEPENDENCIES = ["deconz"]
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||
"""Old way of setting up deCONZ binary sensors."""
|
||||
pass
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up the deCONZ binary sensor."""
|
||||
|
||||
@callback
|
||||
def async_add_sensor(sensors):
|
||||
"""Add binary sensor from deCONZ."""
|
||||
from pydeconz.sensor import DECONZ_BINARY_SENSOR
|
||||
|
||||
entities = []
|
||||
allow_clip_sensor = config_entry.data.get(CONF_ALLOW_CLIP_SENSOR, True)
|
||||
for sensor in sensors:
|
||||
if sensor.type in DECONZ_BINARY_SENSOR and \
|
||||
not (not allow_clip_sensor and sensor.type.startswith('CLIP')):
|
||||
if sensor.type in DECONZ_BINARY_SENSOR and not (
|
||||
not allow_clip_sensor and sensor.type.startswith("CLIP")
|
||||
):
|
||||
entities.append(DeconzBinarySensor(sensor))
|
||||
async_add_entities(entities, True)
|
||||
|
||||
hass.data[DATA_DECONZ_UNSUB].append(
|
||||
async_dispatcher_connect(hass, 'deconz_new_sensor', async_add_sensor))
|
||||
async_dispatcher_connect(hass, "deconz_new_sensor", async_add_sensor)
|
||||
)
|
||||
|
||||
async_add_sensor(hass.data[DATA_DECONZ].sensors.values())
|
||||
|
||||
|
|
@ -66,10 +75,12 @@ class DeconzBinarySensor(BinarySensorDevice):
|
|||
If reason is that state is updated,
|
||||
or reachable has changed or battery has changed.
|
||||
"""
|
||||
if reason['state'] or \
|
||||
'reachable' in reason['attr'] or \
|
||||
'battery' in reason['attr'] or \
|
||||
'on' in reason['attr']:
|
||||
if (
|
||||
reason["state"]
|
||||
or "reachable" in reason["attr"]
|
||||
or "battery" in reason["attr"]
|
||||
or "on" in reason["attr"]
|
||||
):
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
|
|
@ -111,6 +122,7 @@ class DeconzBinarySensor(BinarySensorDevice):
|
|||
def device_state_attributes(self):
|
||||
"""Return the state attributes of the sensor."""
|
||||
from pydeconz.sensor import PRESENCE
|
||||
|
||||
attr = {}
|
||||
if self._sensor.battery:
|
||||
attr[ATTR_BATTERY_LEVEL] = self._sensor.battery
|
||||
|
|
@ -123,15 +135,14 @@ class DeconzBinarySensor(BinarySensorDevice):
|
|||
@property
|
||||
def device_info(self):
|
||||
"""Return a device description for device registry."""
|
||||
if (self._sensor.uniqueid is None or
|
||||
self._sensor.uniqueid.count(':') != 7):
|
||||
if self._sensor.uniqueid is None or self._sensor.uniqueid.count(":") != 7:
|
||||
return None
|
||||
serial = self._sensor.uniqueid.split('-', 1)[0]
|
||||
serial = self._sensor.uniqueid.split("-", 1)[0]
|
||||
return {
|
||||
'connections': {(CONNECTION_ZIGBEE, serial)},
|
||||
'identifiers': {(DECONZ_DOMAIN, serial)},
|
||||
'manufacturer': self._sensor.manufacturer,
|
||||
'model': self._sensor.modelid,
|
||||
'name': self._sensor.name,
|
||||
'sw_version': self._sensor.swversion,
|
||||
"connections": {(CONNECTION_ZIGBEE, serial)},
|
||||
"identifiers": {(DECONZ_DOMAIN, serial)},
|
||||
"manufacturer": self._sensor.manufacturer,
|
||||
"model": self._sensor.modelid,
|
||||
"name": self._sensor.name,
|
||||
"sw_version": self._sensor.swversion,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,10 +9,12 @@ from homeassistant.components.binary_sensor import BinarySensorDevice
|
|||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the Demo binary sensor platform."""
|
||||
add_entities([
|
||||
DemoBinarySensor('Basement Floor Wet', False, 'moisture'),
|
||||
DemoBinarySensor('Movement Backyard', True, 'motion'),
|
||||
])
|
||||
add_entities(
|
||||
[
|
||||
DemoBinarySensor("Basement Floor Wet", False, "moisture"),
|
||||
DemoBinarySensor("Movement Backyard", True, "motion"),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class DemoBinarySensor(BinarySensorDevice):
|
||||
|
|
|
|||
|
|
@ -9,23 +9,32 @@ import logging
|
|||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA
|
||||
from homeassistant.components.digital_ocean import (
|
||||
CONF_DROPLETS, ATTR_CREATED_AT, ATTR_DROPLET_ID, ATTR_DROPLET_NAME,
|
||||
ATTR_FEATURES, ATTR_IPV4_ADDRESS, ATTR_IPV6_ADDRESS, ATTR_MEMORY,
|
||||
ATTR_REGION, ATTR_VCPUS, CONF_ATTRIBUTION, DATA_DIGITAL_OCEAN)
|
||||
CONF_DROPLETS,
|
||||
ATTR_CREATED_AT,
|
||||
ATTR_DROPLET_ID,
|
||||
ATTR_DROPLET_NAME,
|
||||
ATTR_FEATURES,
|
||||
ATTR_IPV4_ADDRESS,
|
||||
ATTR_IPV6_ADDRESS,
|
||||
ATTR_MEMORY,
|
||||
ATTR_REGION,
|
||||
ATTR_VCPUS,
|
||||
CONF_ATTRIBUTION,
|
||||
DATA_DIGITAL_OCEAN,
|
||||
)
|
||||
from homeassistant.const import ATTR_ATTRIBUTION
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = 'Droplet'
|
||||
DEFAULT_DEVICE_CLASS = 'moving'
|
||||
DEPENDENCIES = ['digital_ocean']
|
||||
DEFAULT_NAME = "Droplet"
|
||||
DEFAULT_DEVICE_CLASS = "moving"
|
||||
DEPENDENCIES = ["digital_ocean"]
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_DROPLETS): vol.All(cv.ensure_list, [cv.string]),
|
||||
})
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{vol.Required(CONF_DROPLETS): vol.All(cv.ensure_list, [cv.string])}
|
||||
)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
|
|
@ -65,7 +74,7 @@ class DigitalOceanBinarySensor(BinarySensorDevice):
|
|||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the binary sensor is on."""
|
||||
return self.data.status == 'active'
|
||||
return self.data.status == "active"
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
|
|
@ -84,7 +93,7 @@ class DigitalOceanBinarySensor(BinarySensorDevice):
|
|||
ATTR_IPV4_ADDRESS: self.data.ip_address,
|
||||
ATTR_IPV6_ADDRESS: self.data.ip_v6_address,
|
||||
ATTR_MEMORY: self.data.memory,
|
||||
ATTR_REGION: self.data.region['name'],
|
||||
ATTR_REGION: self.data.region["name"],
|
||||
ATTR_VCPUS: self.data.vcpus,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@ https://home-assistant.io/components/binary_sensor.ecobee/
|
|||
from homeassistant.components import ecobee
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
|
||||
DEPENDENCIES = ['ecobee']
|
||||
DEPENDENCIES = ["ecobee"]
|
||||
|
||||
ECOBEE_CONFIG_FILE = 'ecobee.conf'
|
||||
ECOBEE_CONFIG_FILE = "ecobee.conf"
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
|
|
@ -20,11 +20,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
|||
dev = list()
|
||||
for index in range(len(data.ecobee.thermostats)):
|
||||
for sensor in data.ecobee.get_remote_sensors(index):
|
||||
for item in sensor['capability']:
|
||||
if item['type'] != 'occupancy':
|
||||
for item in sensor["capability"]:
|
||||
if item["type"] != "occupancy":
|
||||
continue
|
||||
|
||||
dev.append(EcobeeBinarySensor(sensor['name'], index))
|
||||
dev.append(EcobeeBinarySensor(sensor["name"], index))
|
||||
|
||||
add_entities(dev, True)
|
||||
|
||||
|
|
@ -34,11 +34,11 @@ class EcobeeBinarySensor(BinarySensorDevice):
|
|||
|
||||
def __init__(self, sensor_name, sensor_index):
|
||||
"""Initialize the sensor."""
|
||||
self._name = sensor_name + ' Occupancy'
|
||||
self._name = sensor_name + " Occupancy"
|
||||
self.sensor_name = sensor_name
|
||||
self.index = sensor_index
|
||||
self._state = None
|
||||
self._device_class = 'occupancy'
|
||||
self._device_class = "occupancy"
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
|
@ -48,7 +48,7 @@ class EcobeeBinarySensor(BinarySensorDevice):
|
|||
@property
|
||||
def is_on(self):
|
||||
"""Return the status of the sensor."""
|
||||
return self._state == 'true'
|
||||
return self._state == "true"
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
|
|
@ -60,7 +60,6 @@ class EcobeeBinarySensor(BinarySensorDevice):
|
|||
data = ecobee.NETWORK
|
||||
data.update()
|
||||
for sensor in data.ecobee.get_remote_sensors(self.index):
|
||||
for item in sensor['capability']:
|
||||
if (item['type'] == 'occupancy' and
|
||||
self.sensor_name == sensor['name']):
|
||||
self._state = item['value']
|
||||
for item in sensor["capability"]:
|
||||
if item["type"] == "occupancy" and self.sensor_name == sensor["name"]:
|
||||
self._state = item["value"]
|
||||
|
|
|
|||
|
|
@ -9,21 +9,21 @@ import logging
|
|||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.const import STATE_ON, STATE_OFF
|
||||
from homeassistant.components.egardia import (
|
||||
EGARDIA_DEVICE, ATTR_DISCOVER_DEVICES)
|
||||
from homeassistant.components.egardia import EGARDIA_DEVICE, ATTR_DISCOVER_DEVICES
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
DEPENDENCIES = ['egardia']
|
||||
EGARDIA_TYPE_TO_DEVICE_CLASS = {'IR Sensor': 'motion',
|
||||
'Door Contact': 'opening',
|
||||
'IR': 'motion'}
|
||||
DEPENDENCIES = ["egardia"]
|
||||
EGARDIA_TYPE_TO_DEVICE_CLASS = {
|
||||
"IR Sensor": "motion",
|
||||
"Door Contact": "opening",
|
||||
"IR": "motion",
|
||||
}
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||
"""Initialize the platform."""
|
||||
if (discovery_info is None or
|
||||
discovery_info[ATTR_DISCOVER_DEVICES] is None):
|
||||
if discovery_info is None or discovery_info[ATTR_DISCOVER_DEVICES] is None:
|
||||
return
|
||||
|
||||
disc_info = discovery_info[ATTR_DISCOVER_DEVICES]
|
||||
|
|
@ -31,14 +31,17 @@ def async_setup_platform(hass, config, async_add_entities,
|
|||
async_add_entities(
|
||||
(
|
||||
EgardiaBinarySensor(
|
||||
sensor_id=disc_info[sensor]['id'],
|
||||
name=disc_info[sensor]['name'],
|
||||
sensor_id=disc_info[sensor]["id"],
|
||||
name=disc_info[sensor]["name"],
|
||||
egardia_system=hass.data[EGARDIA_DEVICE],
|
||||
device_class=EGARDIA_TYPE_TO_DEVICE_CLASS.get(
|
||||
disc_info[sensor]['type'], None)
|
||||
disc_info[sensor]["type"], None
|
||||
),
|
||||
)
|
||||
for sensor in disc_info
|
||||
), True)
|
||||
),
|
||||
True,
|
||||
)
|
||||
|
||||
|
||||
class EgardiaBinarySensor(BinarySensorDevice):
|
||||
|
|
|
|||
|
|
@ -8,20 +8,23 @@ import logging
|
|||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.eight_sleep import (
|
||||
DATA_EIGHT, EightSleepHeatEntity, CONF_BINARY_SENSORS, NAME_MAP)
|
||||
DATA_EIGHT,
|
||||
EightSleepHeatEntity,
|
||||
CONF_BINARY_SENSORS,
|
||||
NAME_MAP,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['eight_sleep']
|
||||
DEPENDENCIES = ["eight_sleep"]
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||
"""Set up the eight sleep binary sensor."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
name = 'Eight'
|
||||
name = "Eight"
|
||||
sensors = discovery_info[CONF_BINARY_SENSORS]
|
||||
eight = hass.data[DATA_EIGHT]
|
||||
|
||||
|
|
@ -42,15 +45,19 @@ class EightHeatSensor(EightSleepHeatEntity, BinarySensorDevice):
|
|||
|
||||
self._sensor = sensor
|
||||
self._mapped_name = NAME_MAP.get(self._sensor, self._sensor)
|
||||
self._name = '{} {}'.format(name, self._mapped_name)
|
||||
self._name = "{} {}".format(name, self._mapped_name)
|
||||
self._state = None
|
||||
|
||||
self._side = self._sensor.split('_')[0]
|
||||
self._side = self._sensor.split("_")[0]
|
||||
self._userid = self._eight.fetch_userid(self._side)
|
||||
self._usrobj = self._eight.users[self._userid]
|
||||
|
||||
_LOGGER.debug("Presence Sensor: %s, Side: %s, User: %s",
|
||||
self._sensor, self._side, self._userid)
|
||||
_LOGGER.debug(
|
||||
"Presence Sensor: %s, Side: %s, User: %s",
|
||||
self._sensor,
|
||||
self._side,
|
||||
self._userid,
|
||||
)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
|
|
|||
|
|
@ -9,22 +9,26 @@ import logging
|
|||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA, DEVICE_CLASSES_SCHEMA)
|
||||
BinarySensorDevice,
|
||||
PLATFORM_SCHEMA,
|
||||
DEVICE_CLASSES_SCHEMA,
|
||||
)
|
||||
from homeassistant.components import enocean
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_ID, CONF_DEVICE_CLASS)
|
||||
from homeassistant.const import CONF_NAME, CONF_ID, CONF_DEVICE_CLASS
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['enocean']
|
||||
DEFAULT_NAME = 'EnOcean binary sensor'
|
||||
DEPENDENCIES = ["enocean"]
|
||||
DEFAULT_NAME = "EnOcean binary sensor"
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_ID): vol.All(cv.ensure_list, [vol.Coerce(int)]),
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
|
|
@ -42,7 +46,7 @@ class EnOceanBinarySensor(enocean.EnOceanDevice, BinarySensorDevice):
|
|||
def __init__(self, dev_id, devname, device_class):
|
||||
"""Initialize the EnOcean binary sensor."""
|
||||
enocean.EnOceanDevice.__init__(self)
|
||||
self.stype = 'listener'
|
||||
self.stype = "listener"
|
||||
self.dev_id = dev_id
|
||||
self.which = -1
|
||||
self.onoff = -1
|
||||
|
|
@ -84,7 +88,12 @@ class EnOceanBinarySensor(enocean.EnOceanDevice, BinarySensorDevice):
|
|||
elif value2 == 0x15:
|
||||
self.which = 10
|
||||
self.onoff = 1
|
||||
self.hass.bus.fire('button_pressed', {'id': self.dev_id,
|
||||
'pushed': value,
|
||||
'which': self.which,
|
||||
'onoff': self.onoff})
|
||||
self.hass.bus.fire(
|
||||
"button_pressed",
|
||||
{
|
||||
"id": self.dev_id,
|
||||
"pushed": value,
|
||||
"which": self.which,
|
||||
"onoff": self.onoff,
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -12,21 +12,25 @@ from homeassistant.core import callback
|
|||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.envisalink import (
|
||||
DATA_EVL, ZONE_SCHEMA, CONF_ZONENAME, CONF_ZONETYPE, EnvisalinkDevice,
|
||||
SIGNAL_ZONE_UPDATE)
|
||||
DATA_EVL,
|
||||
ZONE_SCHEMA,
|
||||
CONF_ZONENAME,
|
||||
CONF_ZONETYPE,
|
||||
EnvisalinkDevice,
|
||||
SIGNAL_ZONE_UPDATE,
|
||||
)
|
||||
from homeassistant.const import ATTR_LAST_TRIP_TIME
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['envisalink']
|
||||
DEPENDENCIES = ["envisalink"]
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||
"""Set up the Envisalink binary sensor devices."""
|
||||
configured_zones = discovery_info['zones']
|
||||
configured_zones = discovery_info["zones"]
|
||||
|
||||
devices = []
|
||||
for zone_num in configured_zones:
|
||||
|
|
@ -36,8 +40,8 @@ def async_setup_platform(hass, config, async_add_entities,
|
|||
zone_num,
|
||||
device_config_data[CONF_ZONENAME],
|
||||
device_config_data[CONF_ZONETYPE],
|
||||
hass.data[DATA_EVL].alarm_state['zone'][zone_num],
|
||||
hass.data[DATA_EVL]
|
||||
hass.data[DATA_EVL].alarm_state["zone"][zone_num],
|
||||
hass.data[DATA_EVL],
|
||||
)
|
||||
devices.append(device)
|
||||
|
||||
|
|
@ -47,20 +51,18 @@ def async_setup_platform(hass, config, async_add_entities,
|
|||
class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice):
|
||||
"""Representation of an Envisalink binary sensor."""
|
||||
|
||||
def __init__(self, hass, zone_number, zone_name, zone_type, info,
|
||||
controller):
|
||||
def __init__(self, hass, zone_number, zone_name, zone_type, info, controller):
|
||||
"""Initialize the binary_sensor."""
|
||||
self._zone_type = zone_type
|
||||
self._zone_number = zone_number
|
||||
|
||||
_LOGGER.debug('Setting up zone: %s', zone_name)
|
||||
_LOGGER.debug("Setting up zone: %s", zone_name)
|
||||
super().__init__(zone_name, info, controller)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
async_dispatcher_connect(
|
||||
self.hass, SIGNAL_ZONE_UPDATE, self._update_callback)
|
||||
async_dispatcher_connect(self.hass, SIGNAL_ZONE_UPDATE, self._update_callback)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
|
|
@ -76,7 +78,7 @@ class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice):
|
|||
# interval, so we subtract it from the current second-accurate time
|
||||
# unless it is already at the maximum value, in which case we set it
|
||||
# to None since we can't determine the actual value.
|
||||
seconds_ago = self._info['last_fault']
|
||||
seconds_ago = self._info["last_fault"]
|
||||
if seconds_ago < 65536 * 5:
|
||||
now = dt_util.now().replace(microsecond=0)
|
||||
delta = datetime.timedelta(seconds=seconds_ago)
|
||||
|
|
@ -90,7 +92,7 @@ class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice):
|
|||
@property
|
||||
def is_on(self):
|
||||
"""Return true if sensor is on."""
|
||||
return self._info['status']['open']
|
||||
return self._info["status"]["open"]
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
|
|
|
|||
|
|
@ -11,44 +11,52 @@ import voluptuous as vol
|
|||
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA
|
||||
from homeassistant.components.ffmpeg import (
|
||||
FFmpegBase, DATA_FFMPEG, CONF_INPUT, CONF_EXTRA_ARGUMENTS,
|
||||
CONF_INITIAL_STATE)
|
||||
FFmpegBase,
|
||||
DATA_FFMPEG,
|
||||
CONF_INPUT,
|
||||
CONF_EXTRA_ARGUMENTS,
|
||||
CONF_INITIAL_STATE,
|
||||
)
|
||||
from homeassistant.const import CONF_NAME
|
||||
|
||||
DEPENDENCIES = ['ffmpeg']
|
||||
DEPENDENCIES = ["ffmpeg"]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_RESET = 'reset'
|
||||
CONF_CHANGES = 'changes'
|
||||
CONF_REPEAT = 'repeat'
|
||||
CONF_REPEAT_TIME = 'repeat_time'
|
||||
CONF_RESET = "reset"
|
||||
CONF_CHANGES = "changes"
|
||||
CONF_REPEAT = "repeat"
|
||||
CONF_REPEAT_TIME = "repeat_time"
|
||||
|
||||
DEFAULT_NAME = 'FFmpeg Motion'
|
||||
DEFAULT_NAME = "FFmpeg Motion"
|
||||
DEFAULT_INIT_STATE = True
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_INPUT): cv.string,
|
||||
vol.Optional(CONF_INITIAL_STATE, default=DEFAULT_INIT_STATE): cv.boolean,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_EXTRA_ARGUMENTS): cv.string,
|
||||
vol.Optional(CONF_RESET, default=10):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=1)),
|
||||
vol.Optional(CONF_CHANGES, default=10):
|
||||
vol.All(vol.Coerce(float), vol.Range(min=0, max=99)),
|
||||
vol.Inclusive(CONF_REPEAT, 'repeat'):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=1)),
|
||||
vol.Inclusive(CONF_REPEAT_TIME, 'repeat'):
|
||||
vol.All(vol.Coerce(int), vol.Range(min=1)),
|
||||
})
|
||||
vol.Optional(CONF_RESET, default=10): vol.All(
|
||||
vol.Coerce(int), vol.Range(min=1)
|
||||
),
|
||||
vol.Optional(CONF_CHANGES, default=10): vol.All(
|
||||
vol.Coerce(float), vol.Range(min=0, max=99)
|
||||
),
|
||||
vol.Inclusive(CONF_REPEAT, "repeat"): vol.All(
|
||||
vol.Coerce(int), vol.Range(min=1)
|
||||
),
|
||||
vol.Inclusive(CONF_REPEAT_TIME, "repeat"): vol.All(
|
||||
vol.Coerce(int), vol.Range(min=1)
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||
"""Set up the FFmpeg binary motion sensor."""
|
||||
manager = hass.data[DATA_FFMPEG]
|
||||
|
||||
|
|
@ -95,8 +103,7 @@ class FFmpegMotion(FFmpegBinarySensor):
|
|||
from haffmpeg import SensorMotion
|
||||
|
||||
super().__init__(config)
|
||||
self.ffmpeg = SensorMotion(
|
||||
manager.binary, hass.loop, self._async_callback)
|
||||
self.ffmpeg = SensorMotion(manager.binary, hass.loop, self._async_callback)
|
||||
|
||||
@asyncio.coroutine
|
||||
def _async_start_ffmpeg(self, entity_ids):
|
||||
|
|
@ -124,4 +131,4 @@ class FFmpegMotion(FFmpegBinarySensor):
|
|||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor, from DEVICE_CLASSES."""
|
||||
return 'motion'
|
||||
return "motion"
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue