Compare commits
1 commit
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d732a7e670 |
1368 changed files with 69529 additions and 53340 deletions
|
|
@ -25,6 +25,7 @@ def attempt_use_uvloop() -> None:
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import uvloop
|
import uvloop
|
||||||
|
|
||||||
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
|
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
|
|
@ -33,28 +34,40 @@ def attempt_use_uvloop() -> None:
|
||||||
def validate_python() -> None:
|
def validate_python() -> None:
|
||||||
"""Validate that the right Python version is running."""
|
"""Validate that the right Python version is running."""
|
||||||
if sys.version_info[:3] < REQUIRED_PYTHON_VER:
|
if sys.version_info[:3] < REQUIRED_PYTHON_VER:
|
||||||
print("Home Assistant requires at least Python {}.{}.{}".format(
|
print(
|
||||||
*REQUIRED_PYTHON_VER))
|
"Home Assistant requires at least Python {}.{}.{}".format(
|
||||||
|
*REQUIRED_PYTHON_VER
|
||||||
|
)
|
||||||
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
def ensure_config_path(config_dir: str) -> None:
|
def ensure_config_path(config_dir: str) -> None:
|
||||||
"""Validate the configuration directory."""
|
"""Validate the configuration directory."""
|
||||||
import homeassistant.config as config_util
|
import homeassistant.config as config_util
|
||||||
lib_dir = os.path.join(config_dir, 'deps')
|
|
||||||
|
lib_dir = os.path.join(config_dir, "deps")
|
||||||
|
|
||||||
# Test if configuration directory exists
|
# Test if configuration directory exists
|
||||||
if not os.path.isdir(config_dir):
|
if not os.path.isdir(config_dir):
|
||||||
if config_dir != config_util.get_default_config_dir():
|
if config_dir != config_util.get_default_config_dir():
|
||||||
print(('Fatal Error: Specified configuration directory does '
|
print(
|
||||||
'not exist {} ').format(config_dir))
|
(
|
||||||
|
"Fatal Error: Specified configuration directory does "
|
||||||
|
"not exist {} "
|
||||||
|
).format(config_dir)
|
||||||
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
os.mkdir(config_dir)
|
os.mkdir(config_dir)
|
||||||
except OSError:
|
except OSError:
|
||||||
print(('Fatal Error: Unable to create default configuration '
|
print(
|
||||||
'directory {} ').format(config_dir))
|
(
|
||||||
|
"Fatal Error: Unable to create default configuration "
|
||||||
|
"directory {} "
|
||||||
|
).format(config_dir)
|
||||||
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# Test if library directory exists
|
# Test if library directory exists
|
||||||
|
|
@ -62,18 +75,22 @@ def ensure_config_path(config_dir: str) -> None:
|
||||||
try:
|
try:
|
||||||
os.mkdir(lib_dir)
|
os.mkdir(lib_dir)
|
||||||
except OSError:
|
except OSError:
|
||||||
print(('Fatal Error: Unable to create library '
|
print(
|
||||||
'directory {} ').format(lib_dir))
|
("Fatal Error: Unable to create library " "directory {} ").format(
|
||||||
|
lib_dir
|
||||||
|
)
|
||||||
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
def ensure_config_file(config_dir: str) -> str:
|
def ensure_config_file(config_dir: str) -> str:
|
||||||
"""Ensure configuration file exists."""
|
"""Ensure configuration file exists."""
|
||||||
import homeassistant.config as config_util
|
import homeassistant.config as config_util
|
||||||
|
|
||||||
config_path = config_util.ensure_config_exists(config_dir)
|
config_path = config_util.ensure_config_exists(config_dir)
|
||||||
|
|
||||||
if config_path is None:
|
if config_path is None:
|
||||||
print('Error getting configuration path')
|
print("Error getting configuration path")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
return config_path
|
return config_path
|
||||||
|
|
@ -82,71 +99,72 @@ def ensure_config_file(config_dir: str) -> str:
|
||||||
def get_arguments() -> argparse.Namespace:
|
def get_arguments() -> argparse.Namespace:
|
||||||
"""Get parsed passed in arguments."""
|
"""Get parsed passed in arguments."""
|
||||||
import homeassistant.config as config_util
|
import homeassistant.config as config_util
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description="Home Assistant: Observe, Control, Automate.")
|
description="Home Assistant: Observe, Control, Automate."
|
||||||
parser.add_argument('--version', action='version', version=__version__)
|
)
|
||||||
|
parser.add_argument("--version", action="version", version=__version__)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'-c', '--config',
|
"-c",
|
||||||
metavar='path_to_config_dir',
|
"--config",
|
||||||
|
metavar="path_to_config_dir",
|
||||||
default=config_util.get_default_config_dir(),
|
default=config_util.get_default_config_dir(),
|
||||||
help="Directory that contains the Home Assistant configuration")
|
help="Directory that contains the Home Assistant configuration",
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--demo-mode',
|
"--demo-mode", action="store_true", help="Start Home Assistant in demo mode"
|
||||||
action='store_true',
|
)
|
||||||
help='Start Home Assistant in demo mode')
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--debug',
|
"--debug", action="store_true", help="Start Home Assistant in debug mode"
|
||||||
action='store_true',
|
)
|
||||||
help='Start Home Assistant in debug mode')
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--open-ui',
|
"--open-ui", action="store_true", help="Open the webinterface in a browser"
|
||||||
action='store_true',
|
)
|
||||||
help='Open the webinterface in a browser')
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--skip-pip',
|
"--skip-pip",
|
||||||
action='store_true',
|
action="store_true",
|
||||||
help='Skips pip install of required packages on startup')
|
help="Skips pip install of required packages on startup",
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'-v', '--verbose',
|
"-v", "--verbose", action="store_true", help="Enable verbose logging to file."
|
||||||
action='store_true',
|
)
|
||||||
help="Enable verbose logging to file.")
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--pid-file',
|
"--pid-file",
|
||||||
metavar='path_to_pid_file',
|
metavar="path_to_pid_file",
|
||||||
default=None,
|
default=None,
|
||||||
help='Path to PID file useful for running as daemon')
|
help="Path to PID file useful for running as daemon",
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--log-rotate-days',
|
"--log-rotate-days",
|
||||||
type=int,
|
type=int,
|
||||||
default=None,
|
default=None,
|
||||||
help='Enables daily log rotation and keeps up to the specified days')
|
help="Enables daily log rotation and keeps up to the specified days",
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--log-file',
|
"--log-file",
|
||||||
type=str,
|
type=str,
|
||||||
default=None,
|
default=None,
|
||||||
help='Log file to write to. If not set, CONFIG/home-assistant.log '
|
help="Log file to write to. If not set, CONFIG/home-assistant.log " "is used",
|
||||||
'is used')
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--log-no-color',
|
"--log-no-color", action="store_true", help="Disable color logs"
|
||||||
action='store_true',
|
)
|
||||||
help="Disable color logs")
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--runner',
|
"--runner",
|
||||||
action='store_true',
|
action="store_true",
|
||||||
help='On restart exit with code {}'.format(RESTART_EXIT_CODE))
|
help="On restart exit with code {}".format(RESTART_EXIT_CODE),
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--script',
|
"--script", nargs=argparse.REMAINDER, help="Run one of the embedded scripts"
|
||||||
nargs=argparse.REMAINDER,
|
)
|
||||||
help='Run one of the embedded scripts')
|
|
||||||
if os.name == "posix":
|
if os.name == "posix":
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--daemon',
|
"--daemon", action="store_true", help="Run Home Assistant as daemon"
|
||||||
action='store_true',
|
)
|
||||||
help='Run Home Assistant as daemon')
|
|
||||||
|
|
||||||
arguments = parser.parse_args()
|
arguments = parser.parse_args()
|
||||||
if os.name != "posix" or arguments.debug or arguments.runner:
|
if os.name != "posix" or arguments.debug or arguments.runner:
|
||||||
setattr(arguments, 'daemon', False)
|
setattr(arguments, "daemon", False)
|
||||||
|
|
||||||
return arguments
|
return arguments
|
||||||
|
|
||||||
|
|
@ -167,8 +185,8 @@ def daemonize() -> None:
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
# redirect standard file descriptors to devnull
|
# redirect standard file descriptors to devnull
|
||||||
infd = open(os.devnull, 'r')
|
infd = open(os.devnull, "r")
|
||||||
outfd = open(os.devnull, 'a+')
|
outfd = open(os.devnull, "a+")
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
sys.stderr.flush()
|
sys.stderr.flush()
|
||||||
os.dup2(infd.fileno(), sys.stdin.fileno())
|
os.dup2(infd.fileno(), sys.stdin.fileno())
|
||||||
|
|
@ -180,7 +198,7 @@ def check_pid(pid_file: str) -> None:
|
||||||
"""Check that Home Assistant is not already running."""
|
"""Check that Home Assistant is not already running."""
|
||||||
# Check pid file
|
# Check pid file
|
||||||
try:
|
try:
|
||||||
with open(pid_file, 'r') as file:
|
with open(pid_file, "r") as file:
|
||||||
pid = int(file.readline())
|
pid = int(file.readline())
|
||||||
except IOError:
|
except IOError:
|
||||||
# PID File does not exist
|
# PID File does not exist
|
||||||
|
|
@ -195,7 +213,7 @@ def check_pid(pid_file: str) -> None:
|
||||||
except OSError:
|
except OSError:
|
||||||
# PID does not exist
|
# PID does not exist
|
||||||
return
|
return
|
||||||
print('Fatal Error: HomeAssistant is already running.')
|
print("Fatal Error: HomeAssistant is already running.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -203,10 +221,10 @@ def write_pid(pid_file: str) -> None:
|
||||||
"""Create a PID File."""
|
"""Create a PID File."""
|
||||||
pid = os.getpid()
|
pid = os.getpid()
|
||||||
try:
|
try:
|
||||||
with open(pid_file, 'w') as file:
|
with open(pid_file, "w") as file:
|
||||||
file.write(str(pid))
|
file.write(str(pid))
|
||||||
except IOError:
|
except IOError:
|
||||||
print('Fatal Error: Unable to write pid file {}'.format(pid_file))
|
print("Fatal Error: Unable to write pid file {}".format(pid_file))
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -230,23 +248,21 @@ def closefds_osx(min_fd: int, max_fd: int) -> None:
|
||||||
|
|
||||||
def cmdline() -> List[str]:
|
def cmdline() -> List[str]:
|
||||||
"""Collect path and arguments to re-execute the current hass instance."""
|
"""Collect path and arguments to re-execute the current hass instance."""
|
||||||
if os.path.basename(sys.argv[0]) == '__main__.py':
|
if os.path.basename(sys.argv[0]) == "__main__.py":
|
||||||
modulepath = os.path.dirname(sys.argv[0])
|
modulepath = os.path.dirname(sys.argv[0])
|
||||||
os.environ['PYTHONPATH'] = os.path.dirname(modulepath)
|
os.environ["PYTHONPATH"] = os.path.dirname(modulepath)
|
||||||
return [sys.executable] + [arg for arg in sys.argv if
|
return [sys.executable] + [arg for arg in sys.argv if arg != "--daemon"]
|
||||||
arg != '--daemon']
|
|
||||||
|
|
||||||
return [arg for arg in sys.argv if arg != '--daemon']
|
return [arg for arg in sys.argv if arg != "--daemon"]
|
||||||
|
|
||||||
|
|
||||||
def setup_and_run_hass(config_dir: str,
|
def setup_and_run_hass(config_dir: str, args: argparse.Namespace) -> int:
|
||||||
args: argparse.Namespace) -> int:
|
|
||||||
"""Set up HASS and run."""
|
"""Set up HASS and run."""
|
||||||
from homeassistant import bootstrap
|
from homeassistant import bootstrap
|
||||||
|
|
||||||
# Run a simple daemon runner process on Windows to handle restarts
|
# Run a simple daemon runner process on Windows to handle restarts
|
||||||
if os.name == 'nt' and '--runner' not in sys.argv:
|
if os.name == "nt" and "--runner" not in sys.argv:
|
||||||
nt_args = cmdline() + ['--runner']
|
nt_args = cmdline() + ["--runner"]
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
subprocess.check_call(nt_args)
|
subprocess.check_call(nt_args)
|
||||||
|
|
@ -256,21 +272,27 @@ def setup_and_run_hass(config_dir: str,
|
||||||
sys.exit(exc.returncode)
|
sys.exit(exc.returncode)
|
||||||
|
|
||||||
if args.demo_mode:
|
if args.demo_mode:
|
||||||
config = {
|
config = {"frontend": {}, "demo": {}} # type: Dict[str, Any]
|
||||||
'frontend': {},
|
|
||||||
'demo': {}
|
|
||||||
} # type: Dict[str, Any]
|
|
||||||
hass = bootstrap.from_config_dict(
|
hass = bootstrap.from_config_dict(
|
||||||
config, config_dir=config_dir, verbose=args.verbose,
|
config,
|
||||||
skip_pip=args.skip_pip, log_rotate_days=args.log_rotate_days,
|
config_dir=config_dir,
|
||||||
log_file=args.log_file, log_no_color=args.log_no_color)
|
verbose=args.verbose,
|
||||||
|
skip_pip=args.skip_pip,
|
||||||
|
log_rotate_days=args.log_rotate_days,
|
||||||
|
log_file=args.log_file,
|
||||||
|
log_no_color=args.log_no_color,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
config_file = ensure_config_file(config_dir)
|
config_file = ensure_config_file(config_dir)
|
||||||
print('Config directory:', config_dir)
|
print("Config directory:", config_dir)
|
||||||
hass = bootstrap.from_config_file(
|
hass = bootstrap.from_config_file(
|
||||||
config_file, verbose=args.verbose, skip_pip=args.skip_pip,
|
config_file,
|
||||||
log_rotate_days=args.log_rotate_days, log_file=args.log_file,
|
verbose=args.verbose,
|
||||||
log_no_color=args.log_no_color)
|
skip_pip=args.skip_pip,
|
||||||
|
log_rotate_days=args.log_rotate_days,
|
||||||
|
log_file=args.log_file,
|
||||||
|
log_no_color=args.log_no_color,
|
||||||
|
)
|
||||||
|
|
||||||
if hass is None:
|
if hass is None:
|
||||||
return -1
|
return -1
|
||||||
|
|
@ -283,12 +305,14 @@ def setup_and_run_hass(config_dir: str,
|
||||||
"""Open the web interface in a browser."""
|
"""Open the web interface in a browser."""
|
||||||
if hass.config.api is not None: # type: ignore
|
if hass.config.api is not None: # type: ignore
|
||||||
import webbrowser
|
import webbrowser
|
||||||
|
|
||||||
webbrowser.open(hass.config.api.base_url) # type: ignore
|
webbrowser.open(hass.config.api.base_url) # type: ignore
|
||||||
|
|
||||||
run_callback_threadsafe(
|
run_callback_threadsafe(
|
||||||
hass.loop,
|
hass.loop,
|
||||||
hass.bus.async_listen_once,
|
hass.bus.async_listen_once,
|
||||||
EVENT_HOMEASSISTANT_START, open_browser
|
EVENT_HOMEASSISTANT_START,
|
||||||
|
open_browser,
|
||||||
)
|
)
|
||||||
|
|
||||||
return hass.start()
|
return hass.start()
|
||||||
|
|
@ -298,17 +322,17 @@ def try_to_restart() -> None:
|
||||||
"""Attempt to clean up state and start a new Home Assistant instance."""
|
"""Attempt to clean up state and start a new Home Assistant instance."""
|
||||||
# Things should be mostly shut down already at this point, now just try
|
# Things should be mostly shut down already at this point, now just try
|
||||||
# to clean up things that may have been left behind.
|
# to clean up things that may have been left behind.
|
||||||
sys.stderr.write('Home Assistant attempting to restart.\n')
|
sys.stderr.write("Home Assistant attempting to restart.\n")
|
||||||
|
|
||||||
# Count remaining threads, ideally there should only be one non-daemonized
|
# Count remaining threads, ideally there should only be one non-daemonized
|
||||||
# thread left (which is us). Nothing we really do with it, but it might be
|
# thread left (which is us). Nothing we really do with it, but it might be
|
||||||
# useful when debugging shutdown/restart issues.
|
# useful when debugging shutdown/restart issues.
|
||||||
try:
|
try:
|
||||||
nthreads = sum(thread.is_alive() and not thread.daemon
|
nthreads = sum(
|
||||||
for thread in threading.enumerate())
|
thread.is_alive() and not thread.daemon for thread in threading.enumerate()
|
||||||
|
)
|
||||||
if nthreads > 1:
|
if nthreads > 1:
|
||||||
sys.stderr.write(
|
sys.stderr.write("Found {} non-daemonic threads.\n".format(nthreads))
|
||||||
"Found {} non-daemonic threads.\n".format(nthreads))
|
|
||||||
|
|
||||||
# Somehow we sometimes seem to trigger an assertion in the python threading
|
# Somehow we sometimes seem to trigger an assertion in the python threading
|
||||||
# module. It seems we find threads that have no associated OS level thread
|
# module. It seems we find threads that have no associated OS level thread
|
||||||
|
|
@ -322,7 +346,7 @@ def try_to_restart() -> None:
|
||||||
except ValueError:
|
except ValueError:
|
||||||
max_fd = 256
|
max_fd = 256
|
||||||
|
|
||||||
if platform.system() == 'Darwin':
|
if platform.system() == "Darwin":
|
||||||
closefds_osx(3, max_fd)
|
closefds_osx(3, max_fd)
|
||||||
else:
|
else:
|
||||||
os.closerange(3, max_fd)
|
os.closerange(3, max_fd)
|
||||||
|
|
@ -341,7 +365,7 @@ def main() -> int:
|
||||||
validate_python()
|
validate_python()
|
||||||
|
|
||||||
monkey_patch_needed = sys.version_info[:3] < (3, 6, 3)
|
monkey_patch_needed = sys.version_info[:3] < (3, 6, 3)
|
||||||
if monkey_patch_needed and os.environ.get('HASS_NO_MONKEY') != '1':
|
if monkey_patch_needed and os.environ.get("HASS_NO_MONKEY") != "1":
|
||||||
if sys.version_info[:2] >= (3, 6):
|
if sys.version_info[:2] >= (3, 6):
|
||||||
monkey_patch.disable_c_asyncio()
|
monkey_patch.disable_c_asyncio()
|
||||||
monkey_patch.patch_weakref_tasks()
|
monkey_patch.patch_weakref_tasks()
|
||||||
|
|
@ -352,6 +376,7 @@ def main() -> int:
|
||||||
|
|
||||||
if args.script is not None:
|
if args.script is not None:
|
||||||
from homeassistant import scripts
|
from homeassistant import scripts
|
||||||
|
|
||||||
return scripts.run(args.script)
|
return scripts.run(args.script)
|
||||||
|
|
||||||
config_dir = os.path.join(os.getcwd(), args.config)
|
config_dir = os.path.join(os.getcwd(), args.config)
|
||||||
|
|
|
||||||
|
|
@ -23,9 +23,10 @@ _ProviderDict = Dict[_ProviderKey, AuthProvider]
|
||||||
|
|
||||||
|
|
||||||
async def auth_manager_from_config(
|
async def auth_manager_from_config(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
provider_configs: List[Dict[str, Any]],
|
provider_configs: List[Dict[str, Any]],
|
||||||
module_configs: List[Dict[str, Any]]) -> 'AuthManager':
|
module_configs: List[Dict[str, Any]],
|
||||||
|
) -> "AuthManager":
|
||||||
"""Initialize an auth manager from config.
|
"""Initialize an auth manager from config.
|
||||||
|
|
||||||
CORE_CONFIG_SCHEMA will make sure do duplicated auth providers or
|
CORE_CONFIG_SCHEMA will make sure do duplicated auth providers or
|
||||||
|
|
@ -34,8 +35,11 @@ async def auth_manager_from_config(
|
||||||
store = auth_store.AuthStore(hass)
|
store = auth_store.AuthStore(hass)
|
||||||
if provider_configs:
|
if provider_configs:
|
||||||
providers = await asyncio.gather(
|
providers = await asyncio.gather(
|
||||||
*[auth_provider_from_config(hass, store, config)
|
*[
|
||||||
for config in provider_configs])
|
auth_provider_from_config(hass, store, config)
|
||||||
|
for config in provider_configs
|
||||||
|
]
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
providers = ()
|
providers = ()
|
||||||
# So returned auth providers are in same order as config
|
# So returned auth providers are in same order as config
|
||||||
|
|
@ -46,8 +50,8 @@ async def auth_manager_from_config(
|
||||||
|
|
||||||
if module_configs:
|
if module_configs:
|
||||||
modules = await asyncio.gather(
|
modules = await asyncio.gather(
|
||||||
*[auth_mfa_module_from_config(hass, config)
|
*[auth_mfa_module_from_config(hass, config) for config in module_configs]
|
||||||
for config in module_configs])
|
)
|
||||||
else:
|
else:
|
||||||
modules = ()
|
modules = ()
|
||||||
# So returned auth modules are in same order as config
|
# So returned auth modules are in same order as config
|
||||||
|
|
@ -62,17 +66,21 @@ async def auth_manager_from_config(
|
||||||
class AuthManager:
|
class AuthManager:
|
||||||
"""Manage the authentication for Home Assistant."""
|
"""Manage the authentication for Home Assistant."""
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant, store: auth_store.AuthStore,
|
def __init__(
|
||||||
providers: _ProviderDict, mfa_modules: _MfaModuleDict) \
|
self,
|
||||||
-> None:
|
hass: HomeAssistant,
|
||||||
|
store: auth_store.AuthStore,
|
||||||
|
providers: _ProviderDict,
|
||||||
|
mfa_modules: _MfaModuleDict,
|
||||||
|
) -> None:
|
||||||
"""Initialize the auth manager."""
|
"""Initialize the auth manager."""
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self._store = store
|
self._store = store
|
||||||
self._providers = providers
|
self._providers = providers
|
||||||
self._mfa_modules = mfa_modules
|
self._mfa_modules = mfa_modules
|
||||||
self.login_flow = data_entry_flow.FlowManager(
|
self.login_flow = data_entry_flow.FlowManager(
|
||||||
hass, self._async_create_login_flow,
|
hass, self._async_create_login_flow, self._async_finish_login_flow
|
||||||
self._async_finish_login_flow)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def active(self) -> bool:
|
def active(self) -> bool:
|
||||||
|
|
@ -87,7 +95,7 @@ class AuthManager:
|
||||||
Should be removed when we removed legacy_api_password auth providers.
|
Should be removed when we removed legacy_api_password auth providers.
|
||||||
"""
|
"""
|
||||||
for provider_type, _ in self._providers:
|
for provider_type, _ in self._providers:
|
||||||
if provider_type == 'legacy_api_password':
|
if provider_type == "legacy_api_password":
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
@ -101,8 +109,7 @@ class AuthManager:
|
||||||
"""Return a list of available auth modules."""
|
"""Return a list of available auth modules."""
|
||||||
return list(self._mfa_modules.values())
|
return list(self._mfa_modules.values())
|
||||||
|
|
||||||
def get_auth_mfa_module(self, module_id: str) \
|
def get_auth_mfa_module(self, module_id: str) -> Optional[MultiFactorAuthModule]:
|
||||||
-> Optional[MultiFactorAuthModule]:
|
|
||||||
"""Return an multi-factor auth module, None if not found."""
|
"""Return an multi-factor auth module, None if not found."""
|
||||||
return self._mfa_modules.get(module_id)
|
return self._mfa_modules.get(module_id)
|
||||||
|
|
||||||
|
|
@ -115,7 +122,8 @@ class AuthManager:
|
||||||
return await self._store.async_get_user(user_id)
|
return await self._store.async_get_user(user_id)
|
||||||
|
|
||||||
async def async_get_user_by_credentials(
|
async def async_get_user_by_credentials(
|
||||||
self, credentials: models.Credentials) -> Optional[models.User]:
|
self, credentials: models.Credentials
|
||||||
|
) -> Optional[models.User]:
|
||||||
"""Get a user by credential, return None if not found."""
|
"""Get a user by credential, return None if not found."""
|
||||||
for user in await self.async_get_users():
|
for user in await self.async_get_users():
|
||||||
for creds in user.credentials:
|
for creds in user.credentials:
|
||||||
|
|
@ -127,49 +135,43 @@ class AuthManager:
|
||||||
async def async_create_system_user(self, name: str) -> models.User:
|
async def async_create_system_user(self, name: str) -> models.User:
|
||||||
"""Create a system user."""
|
"""Create a system user."""
|
||||||
return await self._store.async_create_user(
|
return await self._store.async_create_user(
|
||||||
name=name,
|
name=name, system_generated=True, is_active=True
|
||||||
system_generated=True,
|
|
||||||
is_active=True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_create_user(self, name: str) -> models.User:
|
async def async_create_user(self, name: str) -> models.User:
|
||||||
"""Create a user."""
|
"""Create a user."""
|
||||||
kwargs = {
|
kwargs = {"name": name, "is_active": True} # type: Dict[str, Any]
|
||||||
'name': name,
|
|
||||||
'is_active': True,
|
|
||||||
} # type: Dict[str, Any]
|
|
||||||
|
|
||||||
if await self._user_should_be_owner():
|
if await self._user_should_be_owner():
|
||||||
kwargs['is_owner'] = True
|
kwargs["is_owner"] = True
|
||||||
|
|
||||||
return await self._store.async_create_user(**kwargs)
|
return await self._store.async_create_user(**kwargs)
|
||||||
|
|
||||||
async def async_get_or_create_user(self, credentials: models.Credentials) \
|
async def async_get_or_create_user(
|
||||||
-> models.User:
|
self, credentials: models.Credentials
|
||||||
|
) -> models.User:
|
||||||
"""Get or create a user."""
|
"""Get or create a user."""
|
||||||
if not credentials.is_new:
|
if not credentials.is_new:
|
||||||
user = await self.async_get_user_by_credentials(credentials)
|
user = await self.async_get_user_by_credentials(credentials)
|
||||||
if user is None:
|
if user is None:
|
||||||
raise ValueError('Unable to find the user.')
|
raise ValueError("Unable to find the user.")
|
||||||
else:
|
else:
|
||||||
return user
|
return user
|
||||||
|
|
||||||
auth_provider = self._async_get_auth_provider(credentials)
|
auth_provider = self._async_get_auth_provider(credentials)
|
||||||
|
|
||||||
if auth_provider is None:
|
if auth_provider is None:
|
||||||
raise RuntimeError('Credential with unknown provider encountered')
|
raise RuntimeError("Credential with unknown provider encountered")
|
||||||
|
|
||||||
info = await auth_provider.async_user_meta_for_credentials(
|
info = await auth_provider.async_user_meta_for_credentials(credentials)
|
||||||
credentials)
|
|
||||||
|
|
||||||
return await self._store.async_create_user(
|
return await self._store.async_create_user(
|
||||||
credentials=credentials,
|
credentials=credentials, name=info.name, is_active=info.is_active
|
||||||
name=info.name,
|
|
||||||
is_active=info.is_active,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_link_user(self, user: models.User,
|
async def async_link_user(
|
||||||
credentials: models.Credentials) -> None:
|
self, user: models.User, credentials: models.Credentials
|
||||||
|
) -> None:
|
||||||
"""Link credentials to an existing user."""
|
"""Link credentials to an existing user."""
|
||||||
await self._store.async_link_user(user, credentials)
|
await self._store.async_link_user(user, credentials)
|
||||||
|
|
||||||
|
|
@ -192,47 +194,50 @@ class AuthManager:
|
||||||
async def async_deactivate_user(self, user: models.User) -> None:
|
async def async_deactivate_user(self, user: models.User) -> None:
|
||||||
"""Deactivate a user."""
|
"""Deactivate a user."""
|
||||||
if user.is_owner:
|
if user.is_owner:
|
||||||
raise ValueError('Unable to deactive the owner')
|
raise ValueError("Unable to deactive the owner")
|
||||||
await self._store.async_deactivate_user(user)
|
await self._store.async_deactivate_user(user)
|
||||||
|
|
||||||
async def async_remove_credentials(
|
async def async_remove_credentials(self, credentials: models.Credentials) -> None:
|
||||||
self, credentials: models.Credentials) -> None:
|
|
||||||
"""Remove credentials."""
|
"""Remove credentials."""
|
||||||
provider = self._async_get_auth_provider(credentials)
|
provider = self._async_get_auth_provider(credentials)
|
||||||
|
|
||||||
if (provider is not None and
|
if provider is not None and hasattr(provider, "async_will_remove_credentials"):
|
||||||
hasattr(provider, 'async_will_remove_credentials')):
|
|
||||||
# https://github.com/python/mypy/issues/1424
|
# https://github.com/python/mypy/issues/1424
|
||||||
await provider.async_will_remove_credentials( # type: ignore
|
await provider.async_will_remove_credentials(credentials) # type: ignore
|
||||||
credentials)
|
|
||||||
|
|
||||||
await self._store.async_remove_credentials(credentials)
|
await self._store.async_remove_credentials(credentials)
|
||||||
|
|
||||||
async def async_enable_user_mfa(self, user: models.User,
|
async def async_enable_user_mfa(
|
||||||
mfa_module_id: str, data: Any) -> None:
|
self, user: models.User, mfa_module_id: str, data: Any
|
||||||
|
) -> None:
|
||||||
"""Enable a multi-factor auth module for user."""
|
"""Enable a multi-factor auth module for user."""
|
||||||
if user.system_generated:
|
if user.system_generated:
|
||||||
raise ValueError('System generated users cannot enable '
|
raise ValueError(
|
||||||
'multi-factor auth module.')
|
"System generated users cannot enable " "multi-factor auth module."
|
||||||
|
)
|
||||||
|
|
||||||
module = self.get_auth_mfa_module(mfa_module_id)
|
module = self.get_auth_mfa_module(mfa_module_id)
|
||||||
if module is None:
|
if module is None:
|
||||||
raise ValueError('Unable find multi-factor auth module: {}'
|
raise ValueError(
|
||||||
.format(mfa_module_id))
|
"Unable find multi-factor auth module: {}".format(mfa_module_id)
|
||||||
|
)
|
||||||
|
|
||||||
await module.async_setup_user(user.id, data)
|
await module.async_setup_user(user.id, data)
|
||||||
|
|
||||||
async def async_disable_user_mfa(self, user: models.User,
|
async def async_disable_user_mfa(
|
||||||
mfa_module_id: str) -> None:
|
self, user: models.User, mfa_module_id: str
|
||||||
|
) -> None:
|
||||||
"""Disable a multi-factor auth module for user."""
|
"""Disable a multi-factor auth module for user."""
|
||||||
if user.system_generated:
|
if user.system_generated:
|
||||||
raise ValueError('System generated users cannot disable '
|
raise ValueError(
|
||||||
'multi-factor auth module.')
|
"System generated users cannot disable " "multi-factor auth module."
|
||||||
|
)
|
||||||
|
|
||||||
module = self.get_auth_mfa_module(mfa_module_id)
|
module = self.get_auth_mfa_module(mfa_module_id)
|
||||||
if module is None:
|
if module is None:
|
||||||
raise ValueError('Unable find multi-factor auth module: {}'
|
raise ValueError(
|
||||||
.format(mfa_module_id))
|
"Unable find multi-factor auth module: {}".format(mfa_module_id)
|
||||||
|
)
|
||||||
|
|
||||||
await module.async_depose_user(user.id)
|
await module.async_depose_user(user.id)
|
||||||
|
|
||||||
|
|
@ -245,20 +250,23 @@ class AuthManager:
|
||||||
return modules
|
return modules
|
||||||
|
|
||||||
async def async_create_refresh_token(
|
async def async_create_refresh_token(
|
||||||
self, user: models.User, client_id: Optional[str] = None,
|
self,
|
||||||
client_name: Optional[str] = None,
|
user: models.User,
|
||||||
client_icon: Optional[str] = None,
|
client_id: Optional[str] = None,
|
||||||
token_type: Optional[str] = None,
|
client_name: Optional[str] = None,
|
||||||
access_token_expiration: timedelta = ACCESS_TOKEN_EXPIRATION) \
|
client_icon: Optional[str] = None,
|
||||||
-> models.RefreshToken:
|
token_type: Optional[str] = None,
|
||||||
|
access_token_expiration: timedelta = ACCESS_TOKEN_EXPIRATION,
|
||||||
|
) -> models.RefreshToken:
|
||||||
"""Create a new refresh token for a user."""
|
"""Create a new refresh token for a user."""
|
||||||
if not user.is_active:
|
if not user.is_active:
|
||||||
raise ValueError('User is not active')
|
raise ValueError("User is not active")
|
||||||
|
|
||||||
if user.system_generated and client_id is not None:
|
if user.system_generated and client_id is not None:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
'System generated users cannot have refresh tokens connected '
|
"System generated users cannot have refresh tokens connected "
|
||||||
'to a client.')
|
"to a client."
|
||||||
|
)
|
||||||
|
|
||||||
if token_type is None:
|
if token_type is None:
|
||||||
if user.system_generated:
|
if user.system_generated:
|
||||||
|
|
@ -268,62 +276,77 @@ class AuthManager:
|
||||||
|
|
||||||
if user.system_generated != (token_type == models.TOKEN_TYPE_SYSTEM):
|
if user.system_generated != (token_type == models.TOKEN_TYPE_SYSTEM):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
'System generated users can only have system type '
|
"System generated users can only have system type " "refresh tokens"
|
||||||
'refresh tokens')
|
)
|
||||||
|
|
||||||
if token_type == models.TOKEN_TYPE_NORMAL and client_id is None:
|
if token_type == models.TOKEN_TYPE_NORMAL and client_id is None:
|
||||||
raise ValueError('Client is required to generate a refresh token.')
|
raise ValueError("Client is required to generate a refresh token.")
|
||||||
|
|
||||||
if (token_type == models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN and
|
if (
|
||||||
client_name is None):
|
token_type == models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN
|
||||||
raise ValueError('Client_name is required for long-lived access '
|
and client_name is None
|
||||||
'token')
|
):
|
||||||
|
raise ValueError("Client_name is required for long-lived access " "token")
|
||||||
|
|
||||||
if token_type == models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN:
|
if token_type == models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN:
|
||||||
for token in user.refresh_tokens.values():
|
for token in user.refresh_tokens.values():
|
||||||
if (token.client_name == client_name and token.token_type ==
|
if (
|
||||||
models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN):
|
token.client_name == client_name
|
||||||
|
and token.token_type == models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN
|
||||||
|
):
|
||||||
# Each client_name can only have one
|
# Each client_name can only have one
|
||||||
# long_lived_access_token type of refresh token
|
# long_lived_access_token type of refresh token
|
||||||
raise ValueError('{} already exists'.format(client_name))
|
raise ValueError("{} already exists".format(client_name))
|
||||||
|
|
||||||
return await self._store.async_create_refresh_token(
|
return await self._store.async_create_refresh_token(
|
||||||
user, client_id, client_name, client_icon,
|
user,
|
||||||
token_type, access_token_expiration)
|
client_id,
|
||||||
|
client_name,
|
||||||
|
client_icon,
|
||||||
|
token_type,
|
||||||
|
access_token_expiration,
|
||||||
|
)
|
||||||
|
|
||||||
async def async_get_refresh_token(
|
async def async_get_refresh_token(
|
||||||
self, token_id: str) -> Optional[models.RefreshToken]:
|
self, token_id: str
|
||||||
|
) -> Optional[models.RefreshToken]:
|
||||||
"""Get refresh token by id."""
|
"""Get refresh token by id."""
|
||||||
return await self._store.async_get_refresh_token(token_id)
|
return await self._store.async_get_refresh_token(token_id)
|
||||||
|
|
||||||
async def async_get_refresh_token_by_token(
|
async def async_get_refresh_token_by_token(
|
||||||
self, token: str) -> Optional[models.RefreshToken]:
|
self, token: str
|
||||||
|
) -> Optional[models.RefreshToken]:
|
||||||
"""Get refresh token by token."""
|
"""Get refresh token by token."""
|
||||||
return await self._store.async_get_refresh_token_by_token(token)
|
return await self._store.async_get_refresh_token_by_token(token)
|
||||||
|
|
||||||
async def async_remove_refresh_token(self,
|
async def async_remove_refresh_token(
|
||||||
refresh_token: models.RefreshToken) \
|
self, refresh_token: models.RefreshToken
|
||||||
-> None:
|
) -> None:
|
||||||
"""Delete a refresh token."""
|
"""Delete a refresh token."""
|
||||||
await self._store.async_remove_refresh_token(refresh_token)
|
await self._store.async_remove_refresh_token(refresh_token)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_create_access_token(self,
|
def async_create_access_token(
|
||||||
refresh_token: models.RefreshToken,
|
self, refresh_token: models.RefreshToken, remote_ip: Optional[str] = None
|
||||||
remote_ip: Optional[str] = None) -> str:
|
) -> str:
|
||||||
"""Create a new access token."""
|
"""Create a new access token."""
|
||||||
self._store.async_log_refresh_token_usage(refresh_token, remote_ip)
|
self._store.async_log_refresh_token_usage(refresh_token, remote_ip)
|
||||||
|
|
||||||
# pylint: disable=no-self-use
|
# pylint: disable=no-self-use
|
||||||
now = dt_util.utcnow()
|
now = dt_util.utcnow()
|
||||||
return jwt.encode({
|
return jwt.encode(
|
||||||
'iss': refresh_token.id,
|
{
|
||||||
'iat': now,
|
"iss": refresh_token.id,
|
||||||
'exp': now + refresh_token.access_token_expiration,
|
"iat": now,
|
||||||
}, refresh_token.jwt_key, algorithm='HS256').decode()
|
"exp": now + refresh_token.access_token_expiration,
|
||||||
|
},
|
||||||
|
refresh_token.jwt_key,
|
||||||
|
algorithm="HS256",
|
||||||
|
).decode()
|
||||||
|
|
||||||
async def async_validate_access_token(
|
async def async_validate_access_token(
|
||||||
self, token: str) -> Optional[models.RefreshToken]:
|
self, token: str
|
||||||
|
) -> Optional[models.RefreshToken]:
|
||||||
"""Return refresh token if an access token is valid."""
|
"""Return refresh token if an access token is valid."""
|
||||||
try:
|
try:
|
||||||
unverif_claims = jwt.decode(token, verify=False)
|
unverif_claims = jwt.decode(token, verify=False)
|
||||||
|
|
@ -331,23 +354,18 @@ class AuthManager:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
refresh_token = await self.async_get_refresh_token(
|
refresh_token = await self.async_get_refresh_token(
|
||||||
cast(str, unverif_claims.get('iss')))
|
cast(str, unverif_claims.get("iss"))
|
||||||
|
)
|
||||||
|
|
||||||
if refresh_token is None:
|
if refresh_token is None:
|
||||||
jwt_key = ''
|
jwt_key = ""
|
||||||
issuer = ''
|
issuer = ""
|
||||||
else:
|
else:
|
||||||
jwt_key = refresh_token.jwt_key
|
jwt_key = refresh_token.jwt_key
|
||||||
issuer = refresh_token.id
|
issuer = refresh_token.id
|
||||||
|
|
||||||
try:
|
try:
|
||||||
jwt.decode(
|
jwt.decode(token, jwt_key, leeway=10, issuer=issuer, algorithms=["HS256"])
|
||||||
token,
|
|
||||||
jwt_key,
|
|
||||||
leeway=10,
|
|
||||||
issuer=issuer,
|
|
||||||
algorithms=['HS256']
|
|
||||||
)
|
|
||||||
except jwt.InvalidTokenError:
|
except jwt.InvalidTokenError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
@ -357,31 +375,32 @@ class AuthManager:
|
||||||
return refresh_token
|
return refresh_token
|
||||||
|
|
||||||
async def _async_create_login_flow(
|
async def _async_create_login_flow(
|
||||||
self, handler: _ProviderKey, *, context: Optional[Dict],
|
self, handler: _ProviderKey, *, context: Optional[Dict], data: Optional[Any]
|
||||||
data: Optional[Any]) -> data_entry_flow.FlowHandler:
|
) -> data_entry_flow.FlowHandler:
|
||||||
"""Create a login flow."""
|
"""Create a login flow."""
|
||||||
auth_provider = self._providers[handler]
|
auth_provider = self._providers[handler]
|
||||||
|
|
||||||
return await auth_provider.async_login_flow(context)
|
return await auth_provider.async_login_flow(context)
|
||||||
|
|
||||||
async def _async_finish_login_flow(
|
async def _async_finish_login_flow(
|
||||||
self, flow: LoginFlow, result: Dict[str, Any]) \
|
self, flow: LoginFlow, result: Dict[str, Any]
|
||||||
-> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Return a user as result of login flow."""
|
"""Return a user as result of login flow."""
|
||||||
if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
|
if result["type"] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
|
||||||
return result
|
return result
|
||||||
|
|
||||||
# we got final result
|
# we got final result
|
||||||
if isinstance(result['data'], models.User):
|
if isinstance(result["data"], models.User):
|
||||||
result['result'] = result['data']
|
result["result"] = result["data"]
|
||||||
return result
|
return result
|
||||||
|
|
||||||
auth_provider = self._providers[result['handler']]
|
auth_provider = self._providers[result["handler"]]
|
||||||
credentials = await auth_provider.async_get_or_create_credentials(
|
credentials = await auth_provider.async_get_or_create_credentials(
|
||||||
result['data'])
|
result["data"]
|
||||||
|
)
|
||||||
|
|
||||||
if flow.context is not None and flow.context.get('credential_only'):
|
if flow.context is not None and flow.context.get("credential_only"):
|
||||||
result['result'] = credentials
|
result["result"] = credentials
|
||||||
return result
|
return result
|
||||||
|
|
||||||
# multi-factor module cannot enabled for new credential
|
# multi-factor module cannot enabled for new credential
|
||||||
|
|
@ -396,15 +415,18 @@ class AuthManager:
|
||||||
flow.available_mfa_modules = modules
|
flow.available_mfa_modules = modules
|
||||||
return await flow.async_step_select_mfa_module()
|
return await flow.async_step_select_mfa_module()
|
||||||
|
|
||||||
result['result'] = await self.async_get_or_create_user(credentials)
|
result["result"] = await self.async_get_or_create_user(credentials)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_get_auth_provider(
|
def _async_get_auth_provider(
|
||||||
self, credentials: models.Credentials) -> Optional[AuthProvider]:
|
self, credentials: models.Credentials
|
||||||
|
) -> Optional[AuthProvider]:
|
||||||
"""Get auth provider from a set of credentials."""
|
"""Get auth provider from a set of credentials."""
|
||||||
auth_provider_key = (credentials.auth_provider_type,
|
auth_provider_key = (
|
||||||
credentials.auth_provider_id)
|
credentials.auth_provider_type,
|
||||||
|
credentials.auth_provider_id,
|
||||||
|
)
|
||||||
return self._providers.get(auth_provider_key)
|
return self._providers.get(auth_provider_key)
|
||||||
|
|
||||||
async def _user_should_be_owner(self) -> bool:
|
async def _user_should_be_owner(self) -> bool:
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ from homeassistant.util import dt as dt_util
|
||||||
from . import models
|
from . import models
|
||||||
|
|
||||||
STORAGE_VERSION = 1
|
STORAGE_VERSION = 1
|
||||||
STORAGE_KEY = 'auth'
|
STORAGE_KEY = "auth"
|
||||||
|
|
||||||
|
|
||||||
class AuthStore:
|
class AuthStore:
|
||||||
|
|
@ -47,27 +47,28 @@ class AuthStore:
|
||||||
return self._users.get(user_id)
|
return self._users.get(user_id)
|
||||||
|
|
||||||
async def async_create_user(
|
async def async_create_user(
|
||||||
self, name: Optional[str], is_owner: Optional[bool] = None,
|
self,
|
||||||
is_active: Optional[bool] = None,
|
name: Optional[str],
|
||||||
system_generated: Optional[bool] = None,
|
is_owner: Optional[bool] = None,
|
||||||
credentials: Optional[models.Credentials] = None) -> models.User:
|
is_active: Optional[bool] = None,
|
||||||
|
system_generated: Optional[bool] = None,
|
||||||
|
credentials: Optional[models.Credentials] = None,
|
||||||
|
) -> models.User:
|
||||||
"""Create a new user."""
|
"""Create a new user."""
|
||||||
if self._users is None:
|
if self._users is None:
|
||||||
await self._async_load()
|
await self._async_load()
|
||||||
assert self._users is not None
|
assert self._users is not None
|
||||||
|
|
||||||
kwargs = {
|
kwargs = {"name": name} # type: Dict[str, Any]
|
||||||
'name': name
|
|
||||||
} # type: Dict[str, Any]
|
|
||||||
|
|
||||||
if is_owner is not None:
|
if is_owner is not None:
|
||||||
kwargs['is_owner'] = is_owner
|
kwargs["is_owner"] = is_owner
|
||||||
|
|
||||||
if is_active is not None:
|
if is_active is not None:
|
||||||
kwargs['is_active'] = is_active
|
kwargs["is_active"] = is_active
|
||||||
|
|
||||||
if system_generated is not None:
|
if system_generated is not None:
|
||||||
kwargs['system_generated'] = system_generated
|
kwargs["system_generated"] = system_generated
|
||||||
|
|
||||||
new_user = models.User(**kwargs)
|
new_user = models.User(**kwargs)
|
||||||
|
|
||||||
|
|
@ -81,8 +82,9 @@ class AuthStore:
|
||||||
await self.async_link_user(new_user, credentials)
|
await self.async_link_user(new_user, credentials)
|
||||||
return new_user
|
return new_user
|
||||||
|
|
||||||
async def async_link_user(self, user: models.User,
|
async def async_link_user(
|
||||||
credentials: models.Credentials) -> None:
|
self, user: models.User, credentials: models.Credentials
|
||||||
|
) -> None:
|
||||||
"""Add credentials to an existing user."""
|
"""Add credentials to an existing user."""
|
||||||
user.credentials.append(credentials)
|
user.credentials.append(credentials)
|
||||||
self._async_schedule_save()
|
self._async_schedule_save()
|
||||||
|
|
@ -107,8 +109,7 @@ class AuthStore:
|
||||||
user.is_active = False
|
user.is_active = False
|
||||||
self._async_schedule_save()
|
self._async_schedule_save()
|
||||||
|
|
||||||
async def async_remove_credentials(
|
async def async_remove_credentials(self, credentials: models.Credentials) -> None:
|
||||||
self, credentials: models.Credentials) -> None:
|
|
||||||
"""Remove credentials."""
|
"""Remove credentials."""
|
||||||
if self._users is None:
|
if self._users is None:
|
||||||
await self._async_load()
|
await self._async_load()
|
||||||
|
|
@ -129,23 +130,25 @@ class AuthStore:
|
||||||
self._async_schedule_save()
|
self._async_schedule_save()
|
||||||
|
|
||||||
async def async_create_refresh_token(
|
async def async_create_refresh_token(
|
||||||
self, user: models.User, client_id: Optional[str] = None,
|
self,
|
||||||
client_name: Optional[str] = None,
|
user: models.User,
|
||||||
client_icon: Optional[str] = None,
|
client_id: Optional[str] = None,
|
||||||
token_type: str = models.TOKEN_TYPE_NORMAL,
|
client_name: Optional[str] = None,
|
||||||
access_token_expiration: timedelta = ACCESS_TOKEN_EXPIRATION) \
|
client_icon: Optional[str] = None,
|
||||||
-> models.RefreshToken:
|
token_type: str = models.TOKEN_TYPE_NORMAL,
|
||||||
|
access_token_expiration: timedelta = ACCESS_TOKEN_EXPIRATION,
|
||||||
|
) -> models.RefreshToken:
|
||||||
"""Create a new token for a user."""
|
"""Create a new token for a user."""
|
||||||
kwargs = {
|
kwargs = {
|
||||||
'user': user,
|
"user": user,
|
||||||
'client_id': client_id,
|
"client_id": client_id,
|
||||||
'token_type': token_type,
|
"token_type": token_type,
|
||||||
'access_token_expiration': access_token_expiration
|
"access_token_expiration": access_token_expiration,
|
||||||
} # type: Dict[str, Any]
|
} # type: Dict[str, Any]
|
||||||
if client_name:
|
if client_name:
|
||||||
kwargs['client_name'] = client_name
|
kwargs["client_name"] = client_name
|
||||||
if client_icon:
|
if client_icon:
|
||||||
kwargs['client_icon'] = client_icon
|
kwargs["client_icon"] = client_icon
|
||||||
|
|
||||||
refresh_token = models.RefreshToken(**kwargs)
|
refresh_token = models.RefreshToken(**kwargs)
|
||||||
user.refresh_tokens[refresh_token.id] = refresh_token
|
user.refresh_tokens[refresh_token.id] = refresh_token
|
||||||
|
|
@ -154,7 +157,8 @@ class AuthStore:
|
||||||
return refresh_token
|
return refresh_token
|
||||||
|
|
||||||
async def async_remove_refresh_token(
|
async def async_remove_refresh_token(
|
||||||
self, refresh_token: models.RefreshToken) -> None:
|
self, refresh_token: models.RefreshToken
|
||||||
|
) -> None:
|
||||||
"""Remove a refresh token."""
|
"""Remove a refresh token."""
|
||||||
if self._users is None:
|
if self._users is None:
|
||||||
await self._async_load()
|
await self._async_load()
|
||||||
|
|
@ -166,7 +170,8 @@ class AuthStore:
|
||||||
break
|
break
|
||||||
|
|
||||||
async def async_get_refresh_token(
|
async def async_get_refresh_token(
|
||||||
self, token_id: str) -> Optional[models.RefreshToken]:
|
self, token_id: str
|
||||||
|
) -> Optional[models.RefreshToken]:
|
||||||
"""Get refresh token by id."""
|
"""Get refresh token by id."""
|
||||||
if self._users is None:
|
if self._users is None:
|
||||||
await self._async_load()
|
await self._async_load()
|
||||||
|
|
@ -180,7 +185,8 @@ class AuthStore:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def async_get_refresh_token_by_token(
|
async def async_get_refresh_token_by_token(
|
||||||
self, token: str) -> Optional[models.RefreshToken]:
|
self, token: str
|
||||||
|
) -> Optional[models.RefreshToken]:
|
||||||
"""Get refresh token by token."""
|
"""Get refresh token by token."""
|
||||||
if self._users is None:
|
if self._users is None:
|
||||||
await self._async_load()
|
await self._async_load()
|
||||||
|
|
@ -197,8 +203,8 @@ class AuthStore:
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_log_refresh_token_usage(
|
def async_log_refresh_token_usage(
|
||||||
self, refresh_token: models.RefreshToken,
|
self, refresh_token: models.RefreshToken, remote_ip: Optional[str] = None
|
||||||
remote_ip: Optional[str] = None) -> None:
|
) -> None:
|
||||||
"""Update refresh token last used information."""
|
"""Update refresh token last used information."""
|
||||||
refresh_token.last_used_at = dt_util.utcnow()
|
refresh_token.last_used_at = dt_util.utcnow()
|
||||||
refresh_token.last_used_ip = remote_ip
|
refresh_token.last_used_ip = remote_ip
|
||||||
|
|
@ -219,61 +225,66 @@ class AuthStore:
|
||||||
self._users = users
|
self._users = users
|
||||||
return
|
return
|
||||||
|
|
||||||
for user_dict in data['users']:
|
for user_dict in data["users"]:
|
||||||
users[user_dict['id']] = models.User(**user_dict)
|
users[user_dict["id"]] = models.User(**user_dict)
|
||||||
|
|
||||||
for cred_dict in data['credentials']:
|
for cred_dict in data["credentials"]:
|
||||||
users[cred_dict['user_id']].credentials.append(models.Credentials(
|
users[cred_dict["user_id"]].credentials.append(
|
||||||
id=cred_dict['id'],
|
models.Credentials(
|
||||||
is_new=False,
|
id=cred_dict["id"],
|
||||||
auth_provider_type=cred_dict['auth_provider_type'],
|
is_new=False,
|
||||||
auth_provider_id=cred_dict['auth_provider_id'],
|
auth_provider_type=cred_dict["auth_provider_type"],
|
||||||
data=cred_dict['data'],
|
auth_provider_id=cred_dict["auth_provider_id"],
|
||||||
))
|
data=cred_dict["data"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
for rt_dict in data['refresh_tokens']:
|
for rt_dict in data["refresh_tokens"]:
|
||||||
# Filter out the old keys that don't have jwt_key (pre-0.76)
|
# Filter out the old keys that don't have jwt_key (pre-0.76)
|
||||||
if 'jwt_key' not in rt_dict:
|
if "jwt_key" not in rt_dict:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
created_at = dt_util.parse_datetime(rt_dict['created_at'])
|
created_at = dt_util.parse_datetime(rt_dict["created_at"])
|
||||||
if created_at is None:
|
if created_at is None:
|
||||||
getLogger(__name__).error(
|
getLogger(__name__).error(
|
||||||
'Ignoring refresh token %(id)s with invalid created_at '
|
"Ignoring refresh token %(id)s with invalid created_at "
|
||||||
'%(created_at)s for user_id %(user_id)s', rt_dict)
|
"%(created_at)s for user_id %(user_id)s",
|
||||||
|
rt_dict,
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
token_type = rt_dict.get('token_type')
|
token_type = rt_dict.get("token_type")
|
||||||
if token_type is None:
|
if token_type is None:
|
||||||
if rt_dict['client_id'] is None:
|
if rt_dict["client_id"] is None:
|
||||||
token_type = models.TOKEN_TYPE_SYSTEM
|
token_type = models.TOKEN_TYPE_SYSTEM
|
||||||
else:
|
else:
|
||||||
token_type = models.TOKEN_TYPE_NORMAL
|
token_type = models.TOKEN_TYPE_NORMAL
|
||||||
|
|
||||||
# old refresh_token don't have last_used_at (pre-0.78)
|
# old refresh_token don't have last_used_at (pre-0.78)
|
||||||
last_used_at_str = rt_dict.get('last_used_at')
|
last_used_at_str = rt_dict.get("last_used_at")
|
||||||
if last_used_at_str:
|
if last_used_at_str:
|
||||||
last_used_at = dt_util.parse_datetime(last_used_at_str)
|
last_used_at = dt_util.parse_datetime(last_used_at_str)
|
||||||
else:
|
else:
|
||||||
last_used_at = None
|
last_used_at = None
|
||||||
|
|
||||||
token = models.RefreshToken(
|
token = models.RefreshToken(
|
||||||
id=rt_dict['id'],
|
id=rt_dict["id"],
|
||||||
user=users[rt_dict['user_id']],
|
user=users[rt_dict["user_id"]],
|
||||||
client_id=rt_dict['client_id'],
|
client_id=rt_dict["client_id"],
|
||||||
# use dict.get to keep backward compatibility
|
# use dict.get to keep backward compatibility
|
||||||
client_name=rt_dict.get('client_name'),
|
client_name=rt_dict.get("client_name"),
|
||||||
client_icon=rt_dict.get('client_icon'),
|
client_icon=rt_dict.get("client_icon"),
|
||||||
token_type=token_type,
|
token_type=token_type,
|
||||||
created_at=created_at,
|
created_at=created_at,
|
||||||
access_token_expiration=timedelta(
|
access_token_expiration=timedelta(
|
||||||
seconds=rt_dict['access_token_expiration']),
|
seconds=rt_dict["access_token_expiration"]
|
||||||
token=rt_dict['token'],
|
),
|
||||||
jwt_key=rt_dict['jwt_key'],
|
token=rt_dict["token"],
|
||||||
|
jwt_key=rt_dict["jwt_key"],
|
||||||
last_used_at=last_used_at,
|
last_used_at=last_used_at,
|
||||||
last_used_ip=rt_dict.get('last_used_ip'),
|
last_used_ip=rt_dict.get("last_used_ip"),
|
||||||
)
|
)
|
||||||
users[rt_dict['user_id']].refresh_tokens[token.id] = token
|
users[rt_dict["user_id"]].refresh_tokens[token.id] = token
|
||||||
|
|
||||||
self._users = users
|
self._users = users
|
||||||
|
|
||||||
|
|
@ -292,22 +303,22 @@ class AuthStore:
|
||||||
|
|
||||||
users = [
|
users = [
|
||||||
{
|
{
|
||||||
'id': user.id,
|
"id": user.id,
|
||||||
'is_owner': user.is_owner,
|
"is_owner": user.is_owner,
|
||||||
'is_active': user.is_active,
|
"is_active": user.is_active,
|
||||||
'name': user.name,
|
"name": user.name,
|
||||||
'system_generated': user.system_generated,
|
"system_generated": user.system_generated,
|
||||||
}
|
}
|
||||||
for user in self._users.values()
|
for user in self._users.values()
|
||||||
]
|
]
|
||||||
|
|
||||||
credentials = [
|
credentials = [
|
||||||
{
|
{
|
||||||
'id': credential.id,
|
"id": credential.id,
|
||||||
'user_id': user.id,
|
"user_id": user.id,
|
||||||
'auth_provider_type': credential.auth_provider_type,
|
"auth_provider_type": credential.auth_provider_type,
|
||||||
'auth_provider_id': credential.auth_provider_id,
|
"auth_provider_id": credential.auth_provider_id,
|
||||||
'data': credential.data,
|
"data": credential.data,
|
||||||
}
|
}
|
||||||
for user in self._users.values()
|
for user in self._users.values()
|
||||||
for credential in user.credentials
|
for credential in user.credentials
|
||||||
|
|
@ -315,28 +326,27 @@ class AuthStore:
|
||||||
|
|
||||||
refresh_tokens = [
|
refresh_tokens = [
|
||||||
{
|
{
|
||||||
'id': refresh_token.id,
|
"id": refresh_token.id,
|
||||||
'user_id': user.id,
|
"user_id": user.id,
|
||||||
'client_id': refresh_token.client_id,
|
"client_id": refresh_token.client_id,
|
||||||
'client_name': refresh_token.client_name,
|
"client_name": refresh_token.client_name,
|
||||||
'client_icon': refresh_token.client_icon,
|
"client_icon": refresh_token.client_icon,
|
||||||
'token_type': refresh_token.token_type,
|
"token_type": refresh_token.token_type,
|
||||||
'created_at': refresh_token.created_at.isoformat(),
|
"created_at": refresh_token.created_at.isoformat(),
|
||||||
'access_token_expiration':
|
"access_token_expiration": refresh_token.access_token_expiration.total_seconds(),
|
||||||
refresh_token.access_token_expiration.total_seconds(),
|
"token": refresh_token.token,
|
||||||
'token': refresh_token.token,
|
"jwt_key": refresh_token.jwt_key,
|
||||||
'jwt_key': refresh_token.jwt_key,
|
"last_used_at": refresh_token.last_used_at.isoformat()
|
||||||
'last_used_at':
|
if refresh_token.last_used_at
|
||||||
refresh_token.last_used_at.isoformat()
|
else None,
|
||||||
if refresh_token.last_used_at else None,
|
"last_used_ip": refresh_token.last_used_ip,
|
||||||
'last_used_ip': refresh_token.last_used_ip,
|
|
||||||
}
|
}
|
||||||
for user in self._users.values()
|
for user in self._users.values()
|
||||||
for refresh_token in user.refresh_tokens.values()
|
for refresh_token in user.refresh_tokens.values()
|
||||||
]
|
]
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'users': users,
|
"users": users,
|
||||||
'credentials': credentials,
|
"credentials": credentials,
|
||||||
'refresh_tokens': refresh_tokens,
|
"refresh_tokens": refresh_tokens,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,16 +16,19 @@ from homeassistant.util.decorator import Registry
|
||||||
|
|
||||||
MULTI_FACTOR_AUTH_MODULES = Registry()
|
MULTI_FACTOR_AUTH_MODULES = Registry()
|
||||||
|
|
||||||
MULTI_FACTOR_AUTH_MODULE_SCHEMA = vol.Schema({
|
MULTI_FACTOR_AUTH_MODULE_SCHEMA = vol.Schema(
|
||||||
vol.Required(CONF_TYPE): str,
|
{
|
||||||
vol.Optional(CONF_NAME): str,
|
vol.Required(CONF_TYPE): str,
|
||||||
# Specify ID if you have two mfa auth module for same type.
|
vol.Optional(CONF_NAME): str,
|
||||||
vol.Optional(CONF_ID): str,
|
# Specify ID if you have two mfa auth module for same type.
|
||||||
}, extra=vol.ALLOW_EXTRA)
|
vol.Optional(CONF_ID): str,
|
||||||
|
},
|
||||||
|
extra=vol.ALLOW_EXTRA,
|
||||||
|
)
|
||||||
|
|
||||||
SESSION_EXPIRATION = timedelta(minutes=5)
|
SESSION_EXPIRATION = timedelta(minutes=5)
|
||||||
|
|
||||||
DATA_REQS = 'mfa_auth_module_reqs_processed'
|
DATA_REQS = "mfa_auth_module_reqs_processed"
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -33,7 +36,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||||
class MultiFactorAuthModule:
|
class MultiFactorAuthModule:
|
||||||
"""Multi-factor Auth Module of validation function."""
|
"""Multi-factor Auth Module of validation function."""
|
||||||
|
|
||||||
DEFAULT_TITLE = 'Unnamed auth module'
|
DEFAULT_TITLE = "Unnamed auth module"
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant, config: Dict[str, Any]) -> None:
|
def __init__(self, hass: HomeAssistant, config: Dict[str, Any]) -> None:
|
||||||
"""Initialize an auth module."""
|
"""Initialize an auth module."""
|
||||||
|
|
@ -65,7 +68,7 @@ class MultiFactorAuthModule:
|
||||||
"""Return a voluptuous schema to define mfa auth module's input."""
|
"""Return a voluptuous schema to define mfa auth module's input."""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
async def async_setup_flow(self, user_id: str) -> 'SetupFlow':
|
async def async_setup_flow(self, user_id: str) -> "SetupFlow":
|
||||||
"""Return a data entry flow handler for setup module.
|
"""Return a data entry flow handler for setup module.
|
||||||
|
|
||||||
Mfa module should extend SetupFlow
|
Mfa module should extend SetupFlow
|
||||||
|
|
@ -84,8 +87,7 @@ class MultiFactorAuthModule:
|
||||||
"""Return whether user is setup."""
|
"""Return whether user is setup."""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
async def async_validation(
|
async def async_validation(self, user_id: str, user_input: Dict[str, Any]) -> bool:
|
||||||
self, user_id: str, user_input: Dict[str, Any]) -> bool:
|
|
||||||
"""Return True if validation passed."""
|
"""Return True if validation passed."""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
@ -93,17 +95,17 @@ class MultiFactorAuthModule:
|
||||||
class SetupFlow(data_entry_flow.FlowHandler):
|
class SetupFlow(data_entry_flow.FlowHandler):
|
||||||
"""Handler for the setup flow."""
|
"""Handler for the setup flow."""
|
||||||
|
|
||||||
def __init__(self, auth_module: MultiFactorAuthModule,
|
def __init__(
|
||||||
setup_schema: vol.Schema,
|
self, auth_module: MultiFactorAuthModule, setup_schema: vol.Schema, user_id: str
|
||||||
user_id: str) -> None:
|
) -> None:
|
||||||
"""Initialize the setup flow."""
|
"""Initialize the setup flow."""
|
||||||
self._auth_module = auth_module
|
self._auth_module = auth_module
|
||||||
self._setup_schema = setup_schema
|
self._setup_schema = setup_schema
|
||||||
self._user_id = user_id
|
self._user_id = user_id
|
||||||
|
|
||||||
async def async_step_init(
|
async def async_step_init(
|
||||||
self, user_input: Optional[Dict[str, str]] = None) \
|
self, user_input: Optional[Dict[str, str]] = None
|
||||||
-> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Handle the first step of setup flow.
|
"""Handle the first step of setup flow.
|
||||||
|
|
||||||
Return self.async_show_form(step_id='init') if user_input == None.
|
Return self.async_show_form(step_id='init') if user_input == None.
|
||||||
|
|
@ -112,23 +114,19 @@ class SetupFlow(data_entry_flow.FlowHandler):
|
||||||
errors = {} # type: Dict[str, str]
|
errors = {} # type: Dict[str, str]
|
||||||
|
|
||||||
if user_input:
|
if user_input:
|
||||||
result = await self._auth_module.async_setup_user(
|
result = await self._auth_module.async_setup_user(self._user_id, user_input)
|
||||||
self._user_id, user_input)
|
|
||||||
return self.async_create_entry(
|
return self.async_create_entry(
|
||||||
title=self._auth_module.name,
|
title=self._auth_module.name, data={"result": result}
|
||||||
data={'result': result}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id='init',
|
step_id="init", data_schema=self._setup_schema, errors=errors
|
||||||
data_schema=self._setup_schema,
|
|
||||||
errors=errors
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def auth_mfa_module_from_config(
|
async def auth_mfa_module_from_config(
|
||||||
hass: HomeAssistant, config: Dict[str, Any]) \
|
hass: HomeAssistant, config: Dict[str, Any]
|
||||||
-> MultiFactorAuthModule:
|
) -> MultiFactorAuthModule:
|
||||||
"""Initialize an auth module from a config."""
|
"""Initialize an auth module from a config."""
|
||||||
module_name = config[CONF_TYPE]
|
module_name = config[CONF_TYPE]
|
||||||
module = await _load_mfa_module(hass, module_name)
|
module = await _load_mfa_module(hass, module_name)
|
||||||
|
|
@ -136,26 +134,29 @@ async def auth_mfa_module_from_config(
|
||||||
try:
|
try:
|
||||||
config = module.CONFIG_SCHEMA(config) # type: ignore
|
config = module.CONFIG_SCHEMA(config) # type: ignore
|
||||||
except vol.Invalid as err:
|
except vol.Invalid as err:
|
||||||
_LOGGER.error('Invalid configuration for multi-factor module %s: %s',
|
_LOGGER.error(
|
||||||
module_name, humanize_error(config, err))
|
"Invalid configuration for multi-factor module %s: %s",
|
||||||
|
module_name,
|
||||||
|
humanize_error(config, err),
|
||||||
|
)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
return MULTI_FACTOR_AUTH_MODULES[module_name](hass, config) # type: ignore
|
return MULTI_FACTOR_AUTH_MODULES[module_name](hass, config) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
async def _load_mfa_module(hass: HomeAssistant, module_name: str) \
|
async def _load_mfa_module(hass: HomeAssistant, module_name: str) -> types.ModuleType:
|
||||||
-> types.ModuleType:
|
|
||||||
"""Load an mfa auth module."""
|
"""Load an mfa auth module."""
|
||||||
module_path = 'homeassistant.auth.mfa_modules.{}'.format(module_name)
|
module_path = "homeassistant.auth.mfa_modules.{}".format(module_name)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
module = importlib.import_module(module_path)
|
module = importlib.import_module(module_path)
|
||||||
except ImportError as err:
|
except ImportError as err:
|
||||||
_LOGGER.error('Unable to load mfa module %s: %s', module_name, err)
|
_LOGGER.error("Unable to load mfa module %s: %s", module_name, err)
|
||||||
raise HomeAssistantError('Unable to load mfa module {}: {}'.format(
|
raise HomeAssistantError(
|
||||||
module_name, err))
|
"Unable to load mfa module {}: {}".format(module_name, err)
|
||||||
|
)
|
||||||
|
|
||||||
if hass.config.skip_pip or not hasattr(module, 'REQUIREMENTS'):
|
if hass.config.skip_pip or not hasattr(module, "REQUIREMENTS"):
|
||||||
return module
|
return module
|
||||||
|
|
||||||
processed = hass.data.get(DATA_REQS)
|
processed = hass.data.get(DATA_REQS)
|
||||||
|
|
@ -166,12 +167,13 @@ async def _load_mfa_module(hass: HomeAssistant, module_name: str) \
|
||||||
|
|
||||||
# https://github.com/python/mypy/issues/1424
|
# https://github.com/python/mypy/issues/1424
|
||||||
req_success = await requirements.async_process_requirements(
|
req_success = await requirements.async_process_requirements(
|
||||||
hass, module_path, module.REQUIREMENTS) # type: ignore
|
hass, module_path, module.REQUIREMENTS
|
||||||
|
) # type: ignore
|
||||||
|
|
||||||
if not req_success:
|
if not req_success:
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(
|
||||||
'Unable to process requirements of mfa module {}'.format(
|
"Unable to process requirements of mfa module {}".format(module_name)
|
||||||
module_name))
|
)
|
||||||
|
|
||||||
processed.add(module_name)
|
processed.add(module_name)
|
||||||
return module
|
return module
|
||||||
|
|
|
||||||
|
|
@ -6,39 +6,45 @@ import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
from . import MultiFactorAuthModule, MULTI_FACTOR_AUTH_MODULES, \
|
from . import (
|
||||||
MULTI_FACTOR_AUTH_MODULE_SCHEMA, SetupFlow
|
MultiFactorAuthModule,
|
||||||
|
MULTI_FACTOR_AUTH_MODULES,
|
||||||
|
MULTI_FACTOR_AUTH_MODULE_SCHEMA,
|
||||||
|
SetupFlow,
|
||||||
|
)
|
||||||
|
|
||||||
CONFIG_SCHEMA = MULTI_FACTOR_AUTH_MODULE_SCHEMA.extend({
|
CONFIG_SCHEMA = MULTI_FACTOR_AUTH_MODULE_SCHEMA.extend(
|
||||||
vol.Required('data'): [vol.Schema({
|
{
|
||||||
vol.Required('user_id'): str,
|
vol.Required("data"): [
|
||||||
vol.Required('pin'): str,
|
vol.Schema({vol.Required("user_id"): str, vol.Required("pin"): str})
|
||||||
})]
|
]
|
||||||
}, extra=vol.PREVENT_EXTRA)
|
},
|
||||||
|
extra=vol.PREVENT_EXTRA,
|
||||||
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@MULTI_FACTOR_AUTH_MODULES.register('insecure_example')
|
@MULTI_FACTOR_AUTH_MODULES.register("insecure_example")
|
||||||
class InsecureExampleModule(MultiFactorAuthModule):
|
class InsecureExampleModule(MultiFactorAuthModule):
|
||||||
"""Example auth module validate pin."""
|
"""Example auth module validate pin."""
|
||||||
|
|
||||||
DEFAULT_TITLE = 'Insecure Personal Identify Number'
|
DEFAULT_TITLE = "Insecure Personal Identify Number"
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant, config: Dict[str, Any]) -> None:
|
def __init__(self, hass: HomeAssistant, config: Dict[str, Any]) -> None:
|
||||||
"""Initialize the user data store."""
|
"""Initialize the user data store."""
|
||||||
super().__init__(hass, config)
|
super().__init__(hass, config)
|
||||||
self._data = config['data']
|
self._data = config["data"]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def input_schema(self) -> vol.Schema:
|
def input_schema(self) -> vol.Schema:
|
||||||
"""Validate login flow input data."""
|
"""Validate login flow input data."""
|
||||||
return vol.Schema({'pin': str})
|
return vol.Schema({"pin": str})
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def setup_schema(self) -> vol.Schema:
|
def setup_schema(self) -> vol.Schema:
|
||||||
"""Validate async_setup_user input data."""
|
"""Validate async_setup_user input data."""
|
||||||
return vol.Schema({'pin': str})
|
return vol.Schema({"pin": str})
|
||||||
|
|
||||||
async def async_setup_flow(self, user_id: str) -> SetupFlow:
|
async def async_setup_flow(self, user_id: str) -> SetupFlow:
|
||||||
"""Return a data entry flow handler for setup module.
|
"""Return a data entry flow handler for setup module.
|
||||||
|
|
@ -50,21 +56,21 @@ class InsecureExampleModule(MultiFactorAuthModule):
|
||||||
async def async_setup_user(self, user_id: str, setup_data: Any) -> Any:
|
async def async_setup_user(self, user_id: str, setup_data: Any) -> Any:
|
||||||
"""Set up user to use mfa module."""
|
"""Set up user to use mfa module."""
|
||||||
# data shall has been validate in caller
|
# data shall has been validate in caller
|
||||||
pin = setup_data['pin']
|
pin = setup_data["pin"]
|
||||||
|
|
||||||
for data in self._data:
|
for data in self._data:
|
||||||
if data['user_id'] == user_id:
|
if data["user_id"] == user_id:
|
||||||
# already setup, override
|
# already setup, override
|
||||||
data['pin'] = pin
|
data["pin"] = pin
|
||||||
return
|
return
|
||||||
|
|
||||||
self._data.append({'user_id': user_id, 'pin': pin})
|
self._data.append({"user_id": user_id, "pin": pin})
|
||||||
|
|
||||||
async def async_depose_user(self, user_id: str) -> None:
|
async def async_depose_user(self, user_id: str) -> None:
|
||||||
"""Remove user from mfa module."""
|
"""Remove user from mfa module."""
|
||||||
found = None
|
found = None
|
||||||
for data in self._data:
|
for data in self._data:
|
||||||
if data['user_id'] == user_id:
|
if data["user_id"] == user_id:
|
||||||
found = data
|
found = data
|
||||||
break
|
break
|
||||||
if found:
|
if found:
|
||||||
|
|
@ -73,17 +79,16 @@ class InsecureExampleModule(MultiFactorAuthModule):
|
||||||
async def async_is_user_setup(self, user_id: str) -> bool:
|
async def async_is_user_setup(self, user_id: str) -> bool:
|
||||||
"""Return whether user is setup."""
|
"""Return whether user is setup."""
|
||||||
for data in self._data:
|
for data in self._data:
|
||||||
if data['user_id'] == user_id:
|
if data["user_id"] == user_id:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def async_validation(
|
async def async_validation(self, user_id: str, user_input: Dict[str, Any]) -> bool:
|
||||||
self, user_id: str, user_input: Dict[str, Any]) -> bool:
|
|
||||||
"""Return True if validation passed."""
|
"""Return True if validation passed."""
|
||||||
for data in self._data:
|
for data in self._data:
|
||||||
if data['user_id'] == user_id:
|
if data["user_id"] == user_id:
|
||||||
# user_input has been validate in caller
|
# user_input has been validate in caller
|
||||||
if data['pin'] == user_input['pin']:
|
if data["pin"] == user_input["pin"]:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
|
||||||
|
|
@ -8,23 +8,26 @@ import voluptuous as vol
|
||||||
from homeassistant.auth.models import User
|
from homeassistant.auth.models import User
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
from . import MultiFactorAuthModule, MULTI_FACTOR_AUTH_MODULES, \
|
from . import (
|
||||||
MULTI_FACTOR_AUTH_MODULE_SCHEMA, SetupFlow
|
MultiFactorAuthModule,
|
||||||
|
MULTI_FACTOR_AUTH_MODULES,
|
||||||
|
MULTI_FACTOR_AUTH_MODULE_SCHEMA,
|
||||||
|
SetupFlow,
|
||||||
|
)
|
||||||
|
|
||||||
REQUIREMENTS = ['pyotp==2.2.6', 'PyQRCode==1.2.1']
|
REQUIREMENTS = ["pyotp==2.2.6", "PyQRCode==1.2.1"]
|
||||||
|
|
||||||
CONFIG_SCHEMA = MULTI_FACTOR_AUTH_MODULE_SCHEMA.extend({
|
CONFIG_SCHEMA = MULTI_FACTOR_AUTH_MODULE_SCHEMA.extend({}, extra=vol.PREVENT_EXTRA)
|
||||||
}, extra=vol.PREVENT_EXTRA)
|
|
||||||
|
|
||||||
STORAGE_VERSION = 1
|
STORAGE_VERSION = 1
|
||||||
STORAGE_KEY = 'auth_module.totp'
|
STORAGE_KEY = "auth_module.totp"
|
||||||
STORAGE_USERS = 'users'
|
STORAGE_USERS = "users"
|
||||||
STORAGE_USER_ID = 'user_id'
|
STORAGE_USER_ID = "user_id"
|
||||||
STORAGE_OTA_SECRET = 'ota_secret'
|
STORAGE_OTA_SECRET = "ota_secret"
|
||||||
|
|
||||||
INPUT_FIELD_CODE = 'code'
|
INPUT_FIELD_CODE = "code"
|
||||||
|
|
||||||
DUMMY_SECRET = 'FPPTH34D4E3MI2HG'
|
DUMMY_SECRET = "FPPTH34D4E3MI2HG"
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -37,10 +40,15 @@ def _generate_qr_code(data: str) -> str:
|
||||||
|
|
||||||
with BytesIO() as buffer:
|
with BytesIO() as buffer:
|
||||||
qr_code.svg(file=buffer, scale=4)
|
qr_code.svg(file=buffer, scale=4)
|
||||||
return '{}'.format(
|
return "{}".format(
|
||||||
buffer.getvalue().decode("ascii").replace('\n', '')
|
buffer.getvalue()
|
||||||
.replace('<?xml version="1.0" encoding="UTF-8"?>'
|
.decode("ascii")
|
||||||
'<svg xmlns="http://www.w3.org/2000/svg"', '<svg')
|
.replace("\n", "")
|
||||||
|
.replace(
|
||||||
|
'<?xml version="1.0" encoding="UTF-8"?>'
|
||||||
|
'<svg xmlns="http://www.w3.org/2000/svg"',
|
||||||
|
"<svg",
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -50,23 +58,23 @@ def _generate_secret_and_qr_code(username: str) -> Tuple[str, str, str]:
|
||||||
|
|
||||||
ota_secret = pyotp.random_base32()
|
ota_secret = pyotp.random_base32()
|
||||||
url = pyotp.totp.TOTP(ota_secret).provisioning_uri(
|
url = pyotp.totp.TOTP(ota_secret).provisioning_uri(
|
||||||
username, issuer_name="Home Assistant")
|
username, issuer_name="Home Assistant"
|
||||||
|
)
|
||||||
image = _generate_qr_code(url)
|
image = _generate_qr_code(url)
|
||||||
return ota_secret, url, image
|
return ota_secret, url, image
|
||||||
|
|
||||||
|
|
||||||
@MULTI_FACTOR_AUTH_MODULES.register('totp')
|
@MULTI_FACTOR_AUTH_MODULES.register("totp")
|
||||||
class TotpAuthModule(MultiFactorAuthModule):
|
class TotpAuthModule(MultiFactorAuthModule):
|
||||||
"""Auth module validate time-based one time password."""
|
"""Auth module validate time-based one time password."""
|
||||||
|
|
||||||
DEFAULT_TITLE = 'Time-based One Time Password'
|
DEFAULT_TITLE = "Time-based One Time Password"
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant, config: Dict[str, Any]) -> None:
|
def __init__(self, hass: HomeAssistant, config: Dict[str, Any]) -> None:
|
||||||
"""Initialize the user data store."""
|
"""Initialize the user data store."""
|
||||||
super().__init__(hass, config)
|
super().__init__(hass, config)
|
||||||
self._users = None # type: Optional[Dict[str, str]]
|
self._users = None # type: Optional[Dict[str, str]]
|
||||||
self._user_store = hass.helpers.storage.Store(
|
self._user_store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
|
||||||
STORAGE_VERSION, STORAGE_KEY)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def input_schema(self) -> vol.Schema:
|
def input_schema(self) -> vol.Schema:
|
||||||
|
|
@ -86,14 +94,13 @@ class TotpAuthModule(MultiFactorAuthModule):
|
||||||
"""Save data."""
|
"""Save data."""
|
||||||
await self._user_store.async_save({STORAGE_USERS: self._users})
|
await self._user_store.async_save({STORAGE_USERS: self._users})
|
||||||
|
|
||||||
def _add_ota_secret(self, user_id: str,
|
def _add_ota_secret(self, user_id: str, secret: Optional[str] = None) -> str:
|
||||||
secret: Optional[str] = None) -> str:
|
|
||||||
"""Create a ota_secret for user."""
|
"""Create a ota_secret for user."""
|
||||||
import pyotp
|
import pyotp
|
||||||
|
|
||||||
ota_secret = secret or pyotp.random_base32() # type: str
|
ota_secret = secret or pyotp.random_base32() # type: str
|
||||||
|
|
||||||
self._users[user_id] = ota_secret # type: ignore
|
self._users[user_id] = ota_secret # type: ignore
|
||||||
return ota_secret
|
return ota_secret
|
||||||
|
|
||||||
async def async_setup_flow(self, user_id: str) -> SetupFlow:
|
async def async_setup_flow(self, user_id: str) -> SetupFlow:
|
||||||
|
|
@ -101,7 +108,7 @@ class TotpAuthModule(MultiFactorAuthModule):
|
||||||
|
|
||||||
Mfa module should extend SetupFlow
|
Mfa module should extend SetupFlow
|
||||||
"""
|
"""
|
||||||
user = await self.hass.auth.async_get_user(user_id) # type: ignore
|
user = await self.hass.auth.async_get_user(user_id) # type: ignore
|
||||||
return TotpSetupFlow(self, self.input_schema, user)
|
return TotpSetupFlow(self, self.input_schema, user)
|
||||||
|
|
||||||
async def async_setup_user(self, user_id: str, setup_data: Any) -> str:
|
async def async_setup_user(self, user_id: str, setup_data: Any) -> str:
|
||||||
|
|
@ -110,7 +117,8 @@ class TotpAuthModule(MultiFactorAuthModule):
|
||||||
await self._async_load()
|
await self._async_load()
|
||||||
|
|
||||||
result = await self.hass.async_add_executor_job(
|
result = await self.hass.async_add_executor_job(
|
||||||
self._add_ota_secret, user_id, setup_data.get('secret'))
|
self._add_ota_secret, user_id, setup_data.get("secret")
|
||||||
|
)
|
||||||
|
|
||||||
await self._async_save()
|
await self._async_save()
|
||||||
return result
|
return result
|
||||||
|
|
@ -120,7 +128,7 @@ class TotpAuthModule(MultiFactorAuthModule):
|
||||||
if self._users is None:
|
if self._users is None:
|
||||||
await self._async_load()
|
await self._async_load()
|
||||||
|
|
||||||
if self._users.pop(user_id, None): # type: ignore
|
if self._users.pop(user_id, None): # type: ignore
|
||||||
await self._async_save()
|
await self._async_save()
|
||||||
|
|
||||||
async def async_is_user_setup(self, user_id: str) -> bool:
|
async def async_is_user_setup(self, user_id: str) -> bool:
|
||||||
|
|
@ -128,10 +136,9 @@ class TotpAuthModule(MultiFactorAuthModule):
|
||||||
if self._users is None:
|
if self._users is None:
|
||||||
await self._async_load()
|
await self._async_load()
|
||||||
|
|
||||||
return user_id in self._users # type: ignore
|
return user_id in self._users # type: ignore
|
||||||
|
|
||||||
async def async_validation(
|
async def async_validation(self, user_id: str, user_input: Dict[str, Any]) -> bool:
|
||||||
self, user_id: str, user_input: Dict[str, Any]) -> bool:
|
|
||||||
"""Return True if validation passed."""
|
"""Return True if validation passed."""
|
||||||
if self._users is None:
|
if self._users is None:
|
||||||
await self._async_load()
|
await self._async_load()
|
||||||
|
|
@ -139,7 +146,8 @@ class TotpAuthModule(MultiFactorAuthModule):
|
||||||
# user_input has been validate in caller
|
# user_input has been validate in caller
|
||||||
# set INPUT_FIELD_CODE as vol.Required is not user friendly
|
# set INPUT_FIELD_CODE as vol.Required is not user friendly
|
||||||
return await self.hass.async_add_executor_job(
|
return await self.hass.async_add_executor_job(
|
||||||
self._validate_2fa, user_id, user_input.get(INPUT_FIELD_CODE, ''))
|
self._validate_2fa, user_id, user_input.get(INPUT_FIELD_CODE, "")
|
||||||
|
)
|
||||||
|
|
||||||
def _validate_2fa(self, user_id: str, code: str) -> bool:
|
def _validate_2fa(self, user_id: str, code: str) -> bool:
|
||||||
"""Validate two factor authentication code."""
|
"""Validate two factor authentication code."""
|
||||||
|
|
@ -158,9 +166,9 @@ class TotpAuthModule(MultiFactorAuthModule):
|
||||||
class TotpSetupFlow(SetupFlow):
|
class TotpSetupFlow(SetupFlow):
|
||||||
"""Handler for the setup flow."""
|
"""Handler for the setup flow."""
|
||||||
|
|
||||||
def __init__(self, auth_module: TotpAuthModule,
|
def __init__(
|
||||||
setup_schema: vol.Schema,
|
self, auth_module: TotpAuthModule, setup_schema: vol.Schema, user: User
|
||||||
user: User) -> None:
|
) -> None:
|
||||||
"""Initialize the setup flow."""
|
"""Initialize the setup flow."""
|
||||||
super().__init__(auth_module, setup_schema, user.id)
|
super().__init__(auth_module, setup_schema, user.id)
|
||||||
# to fix typing complaint
|
# to fix typing complaint
|
||||||
|
|
@ -171,8 +179,8 @@ class TotpSetupFlow(SetupFlow):
|
||||||
self._image = None # type Optional[str]
|
self._image = None # type Optional[str]
|
||||||
|
|
||||||
async def async_step_init(
|
async def async_step_init(
|
||||||
self, user_input: Optional[Dict[str, str]] = None) \
|
self, user_input: Optional[Dict[str, str]] = None
|
||||||
-> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Handle the first step of setup flow.
|
"""Handle the first step of setup flow.
|
||||||
|
|
||||||
Return self.async_show_form(step_id='init') if user_input == None.
|
Return self.async_show_form(step_id='init') if user_input == None.
|
||||||
|
|
@ -184,30 +192,31 @@ class TotpSetupFlow(SetupFlow):
|
||||||
|
|
||||||
if user_input:
|
if user_input:
|
||||||
verified = await self.hass.async_add_executor_job( # type: ignore
|
verified = await self.hass.async_add_executor_job( # type: ignore
|
||||||
pyotp.TOTP(self._ota_secret).verify, user_input['code'])
|
pyotp.TOTP(self._ota_secret).verify, user_input["code"]
|
||||||
|
)
|
||||||
if verified:
|
if verified:
|
||||||
result = await self._auth_module.async_setup_user(
|
result = await self._auth_module.async_setup_user(
|
||||||
self._user_id, {'secret': self._ota_secret})
|
self._user_id, {"secret": self._ota_secret}
|
||||||
|
)
|
||||||
return self.async_create_entry(
|
return self.async_create_entry(
|
||||||
title=self._auth_module.name,
|
title=self._auth_module.name, data={"result": result}
|
||||||
data={'result': result}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
errors['base'] = 'invalid_code'
|
errors["base"] = "invalid_code"
|
||||||
|
|
||||||
else:
|
else:
|
||||||
hass = self._auth_module.hass
|
hass = self._auth_module.hass
|
||||||
self._ota_secret, self._url, self._image = \
|
self._ota_secret, self._url, self._image = await hass.async_add_executor_job( # type: ignore
|
||||||
await hass.async_add_executor_job( # type: ignore
|
_generate_secret_and_qr_code, str(self._user.name)
|
||||||
_generate_secret_and_qr_code, str(self._user.name))
|
)
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id='init',
|
step_id="init",
|
||||||
data_schema=self._setup_schema,
|
data_schema=self._setup_schema,
|
||||||
description_placeholders={
|
description_placeholders={
|
||||||
'code': self._ota_secret,
|
"code": self._ota_secret,
|
||||||
'url': self._url,
|
"url": self._url,
|
||||||
'qr_code': self._image
|
"qr_code": self._image,
|
||||||
},
|
},
|
||||||
errors=errors
|
errors=errors,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,9 @@ from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
from .util import generate_secret
|
from .util import generate_secret
|
||||||
|
|
||||||
TOKEN_TYPE_NORMAL = 'normal'
|
TOKEN_TYPE_NORMAL = "normal"
|
||||||
TOKEN_TYPE_SYSTEM = 'system'
|
TOKEN_TYPE_SYSTEM = "system"
|
||||||
TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN = 'long_lived_access_token'
|
TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN = "long_lived_access_token"
|
||||||
|
|
||||||
|
|
||||||
@attr.s(slots=True)
|
@attr.s(slots=True)
|
||||||
|
|
@ -44,16 +44,17 @@ class RefreshToken:
|
||||||
access_token_expiration = attr.ib(type=timedelta)
|
access_token_expiration = attr.ib(type=timedelta)
|
||||||
client_name = attr.ib(type=Optional[str], default=None)
|
client_name = attr.ib(type=Optional[str], default=None)
|
||||||
client_icon = attr.ib(type=Optional[str], default=None)
|
client_icon = attr.ib(type=Optional[str], default=None)
|
||||||
token_type = attr.ib(type=str, default=TOKEN_TYPE_NORMAL,
|
token_type = attr.ib(
|
||||||
validator=attr.validators.in_((
|
type=str,
|
||||||
TOKEN_TYPE_NORMAL, TOKEN_TYPE_SYSTEM,
|
default=TOKEN_TYPE_NORMAL,
|
||||||
TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN)))
|
validator=attr.validators.in_(
|
||||||
|
(TOKEN_TYPE_NORMAL, TOKEN_TYPE_SYSTEM, TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN)
|
||||||
|
),
|
||||||
|
)
|
||||||
id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex))
|
id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex))
|
||||||
created_at = attr.ib(type=datetime, default=attr.Factory(dt_util.utcnow))
|
created_at = attr.ib(type=datetime, default=attr.Factory(dt_util.utcnow))
|
||||||
token = attr.ib(type=str,
|
token = attr.ib(type=str, default=attr.Factory(lambda: generate_secret(64)))
|
||||||
default=attr.Factory(lambda: generate_secret(64)))
|
jwt_key = attr.ib(type=str, default=attr.Factory(lambda: generate_secret(64)))
|
||||||
jwt_key = attr.ib(type=str,
|
|
||||||
default=attr.Factory(lambda: generate_secret(64)))
|
|
||||||
|
|
||||||
last_used_at = attr.ib(type=Optional[datetime], default=None)
|
last_used_at = attr.ib(type=Optional[datetime], default=None)
|
||||||
last_used_ip = attr.ib(type=Optional[str], default=None)
|
last_used_ip = attr.ib(type=Optional[str], default=None)
|
||||||
|
|
@ -73,5 +74,4 @@ class Credentials:
|
||||||
is_new = attr.ib(type=bool, default=True)
|
is_new = attr.ib(type=bool, default=True)
|
||||||
|
|
||||||
|
|
||||||
UserMeta = NamedTuple("UserMeta",
|
UserMeta = NamedTuple("UserMeta", [("name", Optional[str]), ("is_active", bool)])
|
||||||
[('name', Optional[str]), ('is_active', bool)])
|
|
||||||
|
|
|
||||||
|
|
@ -19,25 +19,29 @@ from ..models import Credentials, User, UserMeta # noqa: F401
|
||||||
from ..mfa_modules import SESSION_EXPIRATION
|
from ..mfa_modules import SESSION_EXPIRATION
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
DATA_REQS = 'auth_prov_reqs_processed'
|
DATA_REQS = "auth_prov_reqs_processed"
|
||||||
|
|
||||||
AUTH_PROVIDERS = Registry()
|
AUTH_PROVIDERS = Registry()
|
||||||
|
|
||||||
AUTH_PROVIDER_SCHEMA = vol.Schema({
|
AUTH_PROVIDER_SCHEMA = vol.Schema(
|
||||||
vol.Required(CONF_TYPE): str,
|
{
|
||||||
vol.Optional(CONF_NAME): str,
|
vol.Required(CONF_TYPE): str,
|
||||||
# Specify ID if you have two auth providers for same type.
|
vol.Optional(CONF_NAME): str,
|
||||||
vol.Optional(CONF_ID): str,
|
# Specify ID if you have two auth providers for same type.
|
||||||
}, extra=vol.ALLOW_EXTRA)
|
vol.Optional(CONF_ID): str,
|
||||||
|
},
|
||||||
|
extra=vol.ALLOW_EXTRA,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AuthProvider:
|
class AuthProvider:
|
||||||
"""Provider of user authentication."""
|
"""Provider of user authentication."""
|
||||||
|
|
||||||
DEFAULT_TITLE = 'Unnamed auth provider'
|
DEFAULT_TITLE = "Unnamed auth provider"
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant, store: AuthStore,
|
def __init__(
|
||||||
config: Dict[str, Any]) -> None:
|
self, hass: HomeAssistant, store: AuthStore, config: Dict[str, Any]
|
||||||
|
) -> None:
|
||||||
"""Initialize an auth provider."""
|
"""Initialize an auth provider."""
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self.store = store
|
self.store = store
|
||||||
|
|
@ -73,22 +77,22 @@ class AuthProvider:
|
||||||
credentials
|
credentials
|
||||||
for user in users
|
for user in users
|
||||||
for credentials in user.credentials
|
for credentials in user.credentials
|
||||||
if (credentials.auth_provider_type == self.type and
|
if (
|
||||||
credentials.auth_provider_id == self.id)
|
credentials.auth_provider_type == self.type
|
||||||
|
and credentials.auth_provider_id == self.id
|
||||||
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_create_credentials(self, data: Dict[str, str]) -> Credentials:
|
def async_create_credentials(self, data: Dict[str, str]) -> Credentials:
|
||||||
"""Create credentials."""
|
"""Create credentials."""
|
||||||
return Credentials(
|
return Credentials(
|
||||||
auth_provider_type=self.type,
|
auth_provider_type=self.type, auth_provider_id=self.id, data=data
|
||||||
auth_provider_id=self.id,
|
|
||||||
data=data,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Implement by extending class
|
# Implement by extending class
|
||||||
|
|
||||||
async def async_login_flow(self, context: Optional[Dict]) -> 'LoginFlow':
|
async def async_login_flow(self, context: Optional[Dict]) -> "LoginFlow":
|
||||||
"""Return the data flow for logging in with auth provider.
|
"""Return the data flow for logging in with auth provider.
|
||||||
|
|
||||||
Auth provider should extend LoginFlow and return an instance.
|
Auth provider should extend LoginFlow and return an instance.
|
||||||
|
|
@ -96,12 +100,14 @@ class AuthProvider:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
async def async_get_or_create_credentials(
|
async def async_get_or_create_credentials(
|
||||||
self, flow_result: Dict[str, str]) -> Credentials:
|
self, flow_result: Dict[str, str]
|
||||||
|
) -> Credentials:
|
||||||
"""Get credentials based on the flow result."""
|
"""Get credentials based on the flow result."""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
async def async_user_meta_for_credentials(
|
async def async_user_meta_for_credentials(
|
||||||
self, credentials: Credentials) -> UserMeta:
|
self, credentials: Credentials
|
||||||
|
) -> UserMeta:
|
||||||
"""Return extra user metadata for credentials.
|
"""Return extra user metadata for credentials.
|
||||||
|
|
||||||
Will be used to populate info when creating a new user.
|
Will be used to populate info when creating a new user.
|
||||||
|
|
@ -110,8 +116,8 @@ class AuthProvider:
|
||||||
|
|
||||||
|
|
||||||
async def auth_provider_from_config(
|
async def auth_provider_from_config(
|
||||||
hass: HomeAssistant, store: AuthStore,
|
hass: HomeAssistant, store: AuthStore, config: Dict[str, Any]
|
||||||
config: Dict[str, Any]) -> AuthProvider:
|
) -> AuthProvider:
|
||||||
"""Initialize an auth provider from a config."""
|
"""Initialize an auth provider from a config."""
|
||||||
provider_name = config[CONF_TYPE]
|
provider_name = config[CONF_TYPE]
|
||||||
module = await load_auth_provider_module(hass, provider_name)
|
module = await load_auth_provider_module(hass, provider_name)
|
||||||
|
|
@ -119,25 +125,31 @@ async def auth_provider_from_config(
|
||||||
try:
|
try:
|
||||||
config = module.CONFIG_SCHEMA(config) # type: ignore
|
config = module.CONFIG_SCHEMA(config) # type: ignore
|
||||||
except vol.Invalid as err:
|
except vol.Invalid as err:
|
||||||
_LOGGER.error('Invalid configuration for auth provider %s: %s',
|
_LOGGER.error(
|
||||||
provider_name, humanize_error(config, err))
|
"Invalid configuration for auth provider %s: %s",
|
||||||
|
provider_name,
|
||||||
|
humanize_error(config, err),
|
||||||
|
)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
return AUTH_PROVIDERS[provider_name](hass, store, config) # type: ignore
|
return AUTH_PROVIDERS[provider_name](hass, store, config) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
async def load_auth_provider_module(
|
async def load_auth_provider_module(
|
||||||
hass: HomeAssistant, provider: str) -> types.ModuleType:
|
hass: HomeAssistant, provider: str
|
||||||
|
) -> types.ModuleType:
|
||||||
"""Load an auth provider."""
|
"""Load an auth provider."""
|
||||||
try:
|
try:
|
||||||
module = importlib.import_module(
|
module = importlib.import_module(
|
||||||
'homeassistant.auth.providers.{}'.format(provider))
|
"homeassistant.auth.providers.{}".format(provider)
|
||||||
|
)
|
||||||
except ImportError as err:
|
except ImportError as err:
|
||||||
_LOGGER.error('Unable to load auth provider %s: %s', provider, err)
|
_LOGGER.error("Unable to load auth provider %s: %s", provider, err)
|
||||||
raise HomeAssistantError('Unable to load auth provider {}: {}'.format(
|
raise HomeAssistantError(
|
||||||
provider, err))
|
"Unable to load auth provider {}: {}".format(provider, err)
|
||||||
|
)
|
||||||
|
|
||||||
if hass.config.skip_pip or not hasattr(module, 'REQUIREMENTS'):
|
if hass.config.skip_pip or not hasattr(module, "REQUIREMENTS"):
|
||||||
return module
|
return module
|
||||||
|
|
||||||
processed = hass.data.get(DATA_REQS)
|
processed = hass.data.get(DATA_REQS)
|
||||||
|
|
@ -150,12 +162,13 @@ async def load_auth_provider_module(
|
||||||
# https://github.com/python/mypy/issues/1424
|
# https://github.com/python/mypy/issues/1424
|
||||||
reqs = module.REQUIREMENTS # type: ignore
|
reqs = module.REQUIREMENTS # type: ignore
|
||||||
req_success = await requirements.async_process_requirements(
|
req_success = await requirements.async_process_requirements(
|
||||||
hass, 'auth provider {}'.format(provider), reqs)
|
hass, "auth provider {}".format(provider), reqs
|
||||||
|
)
|
||||||
|
|
||||||
if not req_success:
|
if not req_success:
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(
|
||||||
'Unable to process requirements of auth provider {}'.format(
|
"Unable to process requirements of auth provider {}".format(provider)
|
||||||
provider))
|
)
|
||||||
|
|
||||||
processed.add(provider)
|
processed.add(provider)
|
||||||
return module
|
return module
|
||||||
|
|
@ -174,8 +187,8 @@ class LoginFlow(data_entry_flow.FlowHandler):
|
||||||
self.user = None # type: Optional[User]
|
self.user = None # type: Optional[User]
|
||||||
|
|
||||||
async def async_step_init(
|
async def async_step_init(
|
||||||
self, user_input: Optional[Dict[str, str]] = None) \
|
self, user_input: Optional[Dict[str, str]] = None
|
||||||
-> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Handle the first step of login flow.
|
"""Handle the first step of login flow.
|
||||||
|
|
||||||
Return self.async_show_form(step_id='init') if user_input == None.
|
Return self.async_show_form(step_id='init') if user_input == None.
|
||||||
|
|
@ -184,38 +197,37 @@ class LoginFlow(data_entry_flow.FlowHandler):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
async def async_step_select_mfa_module(
|
async def async_step_select_mfa_module(
|
||||||
self, user_input: Optional[Dict[str, str]] = None) \
|
self, user_input: Optional[Dict[str, str]] = None
|
||||||
-> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Handle the step of select mfa module."""
|
"""Handle the step of select mfa module."""
|
||||||
errors = {}
|
errors = {}
|
||||||
|
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
auth_module = user_input.get('multi_factor_auth_module')
|
auth_module = user_input.get("multi_factor_auth_module")
|
||||||
if auth_module in self.available_mfa_modules:
|
if auth_module in self.available_mfa_modules:
|
||||||
self._auth_module_id = auth_module
|
self._auth_module_id = auth_module
|
||||||
return await self.async_step_mfa()
|
return await self.async_step_mfa()
|
||||||
errors['base'] = 'invalid_auth_module'
|
errors["base"] = "invalid_auth_module"
|
||||||
|
|
||||||
if len(self.available_mfa_modules) == 1:
|
if len(self.available_mfa_modules) == 1:
|
||||||
self._auth_module_id = list(self.available_mfa_modules.keys())[0]
|
self._auth_module_id = list(self.available_mfa_modules.keys())[0]
|
||||||
return await self.async_step_mfa()
|
return await self.async_step_mfa()
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id='select_mfa_module',
|
step_id="select_mfa_module",
|
||||||
data_schema=vol.Schema({
|
data_schema=vol.Schema(
|
||||||
'multi_factor_auth_module': vol.In(self.available_mfa_modules)
|
{"multi_factor_auth_module": vol.In(self.available_mfa_modules)}
|
||||||
}),
|
),
|
||||||
errors=errors,
|
errors=errors,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_step_mfa(
|
async def async_step_mfa(
|
||||||
self, user_input: Optional[Dict[str, str]] = None) \
|
self, user_input: Optional[Dict[str, str]] = None
|
||||||
-> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Handle the step of mfa validation."""
|
"""Handle the step of mfa validation."""
|
||||||
errors = {}
|
errors = {}
|
||||||
|
|
||||||
auth_module = self._auth_manager.get_auth_mfa_module(
|
auth_module = self._auth_manager.get_auth_mfa_module(self._auth_module_id)
|
||||||
self._auth_module_id)
|
|
||||||
if auth_module is None:
|
if auth_module is None:
|
||||||
# Given an invalid input to async_step_select_mfa_module
|
# Given an invalid input to async_step_select_mfa_module
|
||||||
# will show invalid_auth_module error
|
# will show invalid_auth_module error
|
||||||
|
|
@ -224,25 +236,24 @@ class LoginFlow(data_entry_flow.FlowHandler):
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
expires = self.created_at + SESSION_EXPIRATION
|
expires = self.created_at + SESSION_EXPIRATION
|
||||||
if dt_util.utcnow() > expires:
|
if dt_util.utcnow() > expires:
|
||||||
return self.async_abort(
|
return self.async_abort(reason="login_expired")
|
||||||
reason='login_expired'
|
|
||||||
)
|
|
||||||
|
|
||||||
result = await auth_module.async_validation(
|
result = await auth_module.async_validation(
|
||||||
self.user.id, user_input) # type: ignore
|
self.user.id, user_input
|
||||||
|
) # type: ignore
|
||||||
if not result:
|
if not result:
|
||||||
errors['base'] = 'invalid_code'
|
errors["base"] = "invalid_code"
|
||||||
|
|
||||||
if not errors:
|
if not errors:
|
||||||
return await self.async_finish(self.user)
|
return await self.async_finish(self.user)
|
||||||
|
|
||||||
description_placeholders = {
|
description_placeholders = {
|
||||||
'mfa_module_name': auth_module.name,
|
"mfa_module_name": auth_module.name,
|
||||||
'mfa_module_id': auth_module.id
|
"mfa_module_id": auth_module.id,
|
||||||
} # type: Dict[str, str]
|
} # type: Dict[str, str]
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id='mfa',
|
step_id="mfa",
|
||||||
data_schema=auth_module.input_schema,
|
data_schema=auth_module.input_schema,
|
||||||
description_placeholders=description_placeholders,
|
description_placeholders=description_placeholders,
|
||||||
errors=errors,
|
errors=errors,
|
||||||
|
|
@ -250,7 +261,4 @@ class LoginFlow(data_entry_flow.FlowHandler):
|
||||||
|
|
||||||
async def async_finish(self, flow_result: Any) -> Dict:
|
async def async_finish(self, flow_result: Any) -> Dict:
|
||||||
"""Handle the pass of login flow."""
|
"""Handle the pass of login flow."""
|
||||||
return self.async_create_entry(
|
return self.async_create_entry(title=self._auth_provider.name, data=flow_result)
|
||||||
title=self._auth_provider.name,
|
|
||||||
data=flow_result
|
|
||||||
)
|
|
||||||
|
|
|
||||||
|
|
@ -20,14 +20,13 @@ from ..util import generate_secret
|
||||||
|
|
||||||
|
|
||||||
STORAGE_VERSION = 1
|
STORAGE_VERSION = 1
|
||||||
STORAGE_KEY = 'auth_provider.homeassistant'
|
STORAGE_KEY = "auth_provider.homeassistant"
|
||||||
|
|
||||||
|
|
||||||
def _disallow_id(conf: Dict[str, Any]) -> Dict[str, Any]:
|
def _disallow_id(conf: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Disallow ID in config."""
|
"""Disallow ID in config."""
|
||||||
if CONF_ID in conf:
|
if CONF_ID in conf:
|
||||||
raise vol.Invalid(
|
raise vol.Invalid("ID is not allowed for the homeassistant auth provider.")
|
||||||
'ID is not allowed for the homeassistant auth provider.')
|
|
||||||
|
|
||||||
return conf
|
return conf
|
||||||
|
|
||||||
|
|
@ -60,68 +59,62 @@ class Data:
|
||||||
data = await self._store.async_load()
|
data = await self._store.async_load()
|
||||||
|
|
||||||
if data is None:
|
if data is None:
|
||||||
data = {
|
data = {"salt": generate_secret(), "users": []}
|
||||||
'salt': generate_secret(),
|
|
||||||
'users': []
|
|
||||||
}
|
|
||||||
|
|
||||||
self._data = data
|
self._data = data
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def users(self) -> List[Dict[str, str]]:
|
def users(self) -> List[Dict[str, str]]:
|
||||||
"""Return users."""
|
"""Return users."""
|
||||||
return self._data['users'] # type: ignore
|
return self._data["users"] # type: ignore
|
||||||
|
|
||||||
def validate_login(self, username: str, password: str) -> None:
|
def validate_login(self, username: str, password: str) -> None:
|
||||||
"""Validate a username and password.
|
"""Validate a username and password.
|
||||||
|
|
||||||
Raises InvalidAuth if auth invalid.
|
Raises InvalidAuth if auth invalid.
|
||||||
"""
|
"""
|
||||||
dummy = b'$2b$12$CiuFGszHx9eNHxPuQcwBWez4CwDTOcLTX5CbOpV6gef2nYuXkY7BO'
|
dummy = b"$2b$12$CiuFGszHx9eNHxPuQcwBWez4CwDTOcLTX5CbOpV6gef2nYuXkY7BO"
|
||||||
found = None
|
found = None
|
||||||
|
|
||||||
# Compare all users to avoid timing attacks.
|
# Compare all users to avoid timing attacks.
|
||||||
for user in self.users:
|
for user in self.users:
|
||||||
if username == user['username']:
|
if username == user["username"]:
|
||||||
found = user
|
found = user
|
||||||
|
|
||||||
if found is None:
|
if found is None:
|
||||||
# check a hash to make timing the same as if user was found
|
# check a hash to make timing the same as if user was found
|
||||||
bcrypt.checkpw(b'foo',
|
bcrypt.checkpw(b"foo", dummy)
|
||||||
dummy)
|
|
||||||
raise InvalidAuth
|
raise InvalidAuth
|
||||||
|
|
||||||
user_hash = base64.b64decode(found['password'])
|
user_hash = base64.b64decode(found["password"])
|
||||||
|
|
||||||
# if the hash is not a bcrypt hash...
|
# if the hash is not a bcrypt hash...
|
||||||
# provide a transparant upgrade for old pbkdf2 hash format
|
# provide a transparant upgrade for old pbkdf2 hash format
|
||||||
if not (user_hash.startswith(b'$2a$')
|
if not (
|
||||||
or user_hash.startswith(b'$2b$')
|
user_hash.startswith(b"$2a$")
|
||||||
or user_hash.startswith(b'$2x$')
|
or user_hash.startswith(b"$2b$")
|
||||||
or user_hash.startswith(b'$2y$')):
|
or user_hash.startswith(b"$2x$")
|
||||||
|
or user_hash.startswith(b"$2y$")
|
||||||
|
):
|
||||||
# IMPORTANT! validate the login, bail if invalid
|
# IMPORTANT! validate the login, bail if invalid
|
||||||
hashed = self.legacy_hash_password(password)
|
hashed = self.legacy_hash_password(password)
|
||||||
if not hmac.compare_digest(hashed, user_hash):
|
if not hmac.compare_digest(hashed, user_hash):
|
||||||
raise InvalidAuth
|
raise InvalidAuth
|
||||||
# then re-hash the valid password with bcrypt
|
# then re-hash the valid password with bcrypt
|
||||||
self.change_password(found['username'], password)
|
self.change_password(found["username"], password)
|
||||||
run_coroutine_threadsafe(
|
run_coroutine_threadsafe(self.async_save(), self.hass.loop).result()
|
||||||
self.async_save(), self.hass.loop
|
user_hash = base64.b64decode(found["password"])
|
||||||
).result()
|
|
||||||
user_hash = base64.b64decode(found['password'])
|
|
||||||
|
|
||||||
# bcrypt.checkpw is timing-safe
|
# bcrypt.checkpw is timing-safe
|
||||||
if not bcrypt.checkpw(password.encode(),
|
if not bcrypt.checkpw(password.encode(), user_hash):
|
||||||
user_hash):
|
|
||||||
raise InvalidAuth
|
raise InvalidAuth
|
||||||
|
|
||||||
def legacy_hash_password(self, password: str,
|
def legacy_hash_password(self, password: str, for_storage: bool = False) -> bytes:
|
||||||
for_storage: bool = False) -> bytes:
|
|
||||||
"""LEGACY password encoding."""
|
"""LEGACY password encoding."""
|
||||||
# We're no longer storing salts in data, but if one exists we
|
# We're no longer storing salts in data, but if one exists we
|
||||||
# should be able to retrieve it.
|
# should be able to retrieve it.
|
||||||
salt = self._data['salt'].encode() # type: ignore
|
salt = self._data["salt"].encode() # type: ignore
|
||||||
hashed = hashlib.pbkdf2_hmac('sha512', password.encode(), salt, 100000)
|
hashed = hashlib.pbkdf2_hmac("sha512", password.encode(), salt, 100000)
|
||||||
if for_storage:
|
if for_storage:
|
||||||
hashed = base64.b64encode(hashed)
|
hashed = base64.b64encode(hashed)
|
||||||
return hashed
|
return hashed
|
||||||
|
|
@ -129,28 +122,30 @@ class Data:
|
||||||
# pylint: disable=no-self-use
|
# pylint: disable=no-self-use
|
||||||
def hash_password(self, password: str, for_storage: bool = False) -> bytes:
|
def hash_password(self, password: str, for_storage: bool = False) -> bytes:
|
||||||
"""Encode a password."""
|
"""Encode a password."""
|
||||||
hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12)) \
|
hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12))
|
||||||
# type: bytes
|
# type: bytes
|
||||||
if for_storage:
|
if for_storage:
|
||||||
hashed = base64.b64encode(hashed)
|
hashed = base64.b64encode(hashed)
|
||||||
return hashed
|
return hashed
|
||||||
|
|
||||||
def add_auth(self, username: str, password: str) -> None:
|
def add_auth(self, username: str, password: str) -> None:
|
||||||
"""Add a new authenticated user/pass."""
|
"""Add a new authenticated user/pass."""
|
||||||
if any(user['username'] == username for user in self.users):
|
if any(user["username"] == username for user in self.users):
|
||||||
raise InvalidUser
|
raise InvalidUser
|
||||||
|
|
||||||
self.users.append({
|
self.users.append(
|
||||||
'username': username,
|
{
|
||||||
'password': self.hash_password(password, True).decode(),
|
"username": username,
|
||||||
})
|
"password": self.hash_password(password, True).decode(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_remove_auth(self, username: str) -> None:
|
def async_remove_auth(self, username: str) -> None:
|
||||||
"""Remove authentication."""
|
"""Remove authentication."""
|
||||||
index = None
|
index = None
|
||||||
for i, user in enumerate(self.users):
|
for i, user in enumerate(self.users):
|
||||||
if user['username'] == username:
|
if user["username"] == username:
|
||||||
index = i
|
index = i
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
@ -165,9 +160,8 @@ class Data:
|
||||||
Raises InvalidUser if user cannot be found.
|
Raises InvalidUser if user cannot be found.
|
||||||
"""
|
"""
|
||||||
for user in self.users:
|
for user in self.users:
|
||||||
if user['username'] == username:
|
if user["username"] == username:
|
||||||
user['password'] = self.hash_password(
|
user["password"] = self.hash_password(new_password, True).decode()
|
||||||
new_password, True).decode()
|
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
raise InvalidUser
|
raise InvalidUser
|
||||||
|
|
@ -177,11 +171,11 @@ class Data:
|
||||||
await self._store.async_save(self._data)
|
await self._store.async_save(self._data)
|
||||||
|
|
||||||
|
|
||||||
@AUTH_PROVIDERS.register('homeassistant')
|
@AUTH_PROVIDERS.register("homeassistant")
|
||||||
class HassAuthProvider(AuthProvider):
|
class HassAuthProvider(AuthProvider):
|
||||||
"""Auth provider based on a local storage of users in HASS config dir."""
|
"""Auth provider based on a local storage of users in HASS config dir."""
|
||||||
|
|
||||||
DEFAULT_TITLE = 'Home Assistant Local'
|
DEFAULT_TITLE = "Home Assistant Local"
|
||||||
|
|
||||||
data = None
|
data = None
|
||||||
|
|
||||||
|
|
@ -193,8 +187,7 @@ class HassAuthProvider(AuthProvider):
|
||||||
self.data = Data(self.hass)
|
self.data = Data(self.hass)
|
||||||
await self.data.async_load()
|
await self.data.async_load()
|
||||||
|
|
||||||
async def async_login_flow(
|
async def async_login_flow(self, context: Optional[Dict]) -> LoginFlow:
|
||||||
self, context: Optional[Dict]) -> LoginFlow:
|
|
||||||
"""Return a flow to login."""
|
"""Return a flow to login."""
|
||||||
return HassLoginFlow(self)
|
return HassLoginFlow(self)
|
||||||
|
|
||||||
|
|
@ -205,36 +198,36 @@ class HassAuthProvider(AuthProvider):
|
||||||
assert self.data is not None
|
assert self.data is not None
|
||||||
|
|
||||||
await self.hass.async_add_executor_job(
|
await self.hass.async_add_executor_job(
|
||||||
self.data.validate_login, username, password)
|
self.data.validate_login, username, password
|
||||||
|
)
|
||||||
|
|
||||||
async def async_get_or_create_credentials(
|
async def async_get_or_create_credentials(
|
||||||
self, flow_result: Dict[str, str]) -> Credentials:
|
self, flow_result: Dict[str, str]
|
||||||
|
) -> Credentials:
|
||||||
"""Get credentials based on the flow result."""
|
"""Get credentials based on the flow result."""
|
||||||
username = flow_result['username']
|
username = flow_result["username"]
|
||||||
|
|
||||||
for credential in await self.async_credentials():
|
for credential in await self.async_credentials():
|
||||||
if credential.data['username'] == username:
|
if credential.data["username"] == username:
|
||||||
return credential
|
return credential
|
||||||
|
|
||||||
# Create new credentials.
|
# Create new credentials.
|
||||||
return self.async_create_credentials({
|
return self.async_create_credentials({"username": username})
|
||||||
'username': username
|
|
||||||
})
|
|
||||||
|
|
||||||
async def async_user_meta_for_credentials(
|
async def async_user_meta_for_credentials(
|
||||||
self, credentials: Credentials) -> UserMeta:
|
self, credentials: Credentials
|
||||||
|
) -> UserMeta:
|
||||||
"""Get extra info for this credential."""
|
"""Get extra info for this credential."""
|
||||||
return UserMeta(name=credentials.data['username'], is_active=True)
|
return UserMeta(name=credentials.data["username"], is_active=True)
|
||||||
|
|
||||||
async def async_will_remove_credentials(
|
async def async_will_remove_credentials(self, credentials: Credentials) -> None:
|
||||||
self, credentials: Credentials) -> None:
|
|
||||||
"""When credentials get removed, also remove the auth."""
|
"""When credentials get removed, also remove the auth."""
|
||||||
if self.data is None:
|
if self.data is None:
|
||||||
await self.async_initialize()
|
await self.async_initialize()
|
||||||
assert self.data is not None
|
assert self.data is not None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.data.async_remove_auth(credentials.data['username'])
|
self.data.async_remove_auth(credentials.data["username"])
|
||||||
await self.data.async_save()
|
await self.data.async_save()
|
||||||
except InvalidUser:
|
except InvalidUser:
|
||||||
# Can happen if somehow we didn't clean up a credential
|
# Can happen if somehow we didn't clean up a credential
|
||||||
|
|
@ -245,29 +238,27 @@ class HassLoginFlow(LoginFlow):
|
||||||
"""Handler for the login flow."""
|
"""Handler for the login flow."""
|
||||||
|
|
||||||
async def async_step_init(
|
async def async_step_init(
|
||||||
self, user_input: Optional[Dict[str, str]] = None) \
|
self, user_input: Optional[Dict[str, str]] = None
|
||||||
-> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Handle the step of the form."""
|
"""Handle the step of the form."""
|
||||||
errors = {}
|
errors = {}
|
||||||
|
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
try:
|
try:
|
||||||
await cast(HassAuthProvider, self._auth_provider)\
|
await cast(HassAuthProvider, self._auth_provider).async_validate_login(
|
||||||
.async_validate_login(user_input['username'],
|
user_input["username"], user_input["password"]
|
||||||
user_input['password'])
|
)
|
||||||
except InvalidAuth:
|
except InvalidAuth:
|
||||||
errors['base'] = 'invalid_auth'
|
errors["base"] = "invalid_auth"
|
||||||
|
|
||||||
if not errors:
|
if not errors:
|
||||||
user_input.pop('password')
|
user_input.pop("password")
|
||||||
return await self.async_finish(user_input)
|
return await self.async_finish(user_input)
|
||||||
|
|
||||||
schema = OrderedDict() # type: Dict[str, type]
|
schema = OrderedDict() # type: Dict[str, type]
|
||||||
schema['username'] = str
|
schema["username"] = str
|
||||||
schema['password'] = str
|
schema["password"] = str
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id='init',
|
step_id="init", data_schema=vol.Schema(schema), errors=errors
|
||||||
data_schema=vol.Schema(schema),
|
|
||||||
errors=errors,
|
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -12,23 +12,25 @@ from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, LoginFlow
|
||||||
from ..models import Credentials, UserMeta
|
from ..models import Credentials, UserMeta
|
||||||
|
|
||||||
|
|
||||||
USER_SCHEMA = vol.Schema({
|
USER_SCHEMA = vol.Schema(
|
||||||
vol.Required('username'): str,
|
{
|
||||||
vol.Required('password'): str,
|
vol.Required("username"): str,
|
||||||
vol.Optional('name'): str,
|
vol.Required("password"): str,
|
||||||
})
|
vol.Optional("name"): str,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend({
|
CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend(
|
||||||
vol.Required('users'): [USER_SCHEMA]
|
{vol.Required("users"): [USER_SCHEMA]}, extra=vol.PREVENT_EXTRA
|
||||||
}, extra=vol.PREVENT_EXTRA)
|
)
|
||||||
|
|
||||||
|
|
||||||
class InvalidAuthError(HomeAssistantError):
|
class InvalidAuthError(HomeAssistantError):
|
||||||
"""Raised when submitting invalid authentication."""
|
"""Raised when submitting invalid authentication."""
|
||||||
|
|
||||||
|
|
||||||
@AUTH_PROVIDERS.register('insecure_example')
|
@AUTH_PROVIDERS.register("insecure_example")
|
||||||
class ExampleAuthProvider(AuthProvider):
|
class ExampleAuthProvider(AuthProvider):
|
||||||
"""Example auth provider based on hardcoded usernames and passwords."""
|
"""Example auth provider based on hardcoded usernames and passwords."""
|
||||||
|
|
||||||
|
|
@ -42,47 +44,48 @@ class ExampleAuthProvider(AuthProvider):
|
||||||
user = None
|
user = None
|
||||||
|
|
||||||
# Compare all users to avoid timing attacks.
|
# Compare all users to avoid timing attacks.
|
||||||
for usr in self.config['users']:
|
for usr in self.config["users"]:
|
||||||
if hmac.compare_digest(username.encode('utf-8'),
|
if hmac.compare_digest(
|
||||||
usr['username'].encode('utf-8')):
|
username.encode("utf-8"), usr["username"].encode("utf-8")
|
||||||
|
):
|
||||||
user = usr
|
user = usr
|
||||||
|
|
||||||
if user is None:
|
if user is None:
|
||||||
# Do one more compare to make timing the same as if user was found.
|
# Do one more compare to make timing the same as if user was found.
|
||||||
hmac.compare_digest(password.encode('utf-8'),
|
hmac.compare_digest(password.encode("utf-8"), password.encode("utf-8"))
|
||||||
password.encode('utf-8'))
|
|
||||||
raise InvalidAuthError
|
raise InvalidAuthError
|
||||||
|
|
||||||
if not hmac.compare_digest(user['password'].encode('utf-8'),
|
if not hmac.compare_digest(
|
||||||
password.encode('utf-8')):
|
user["password"].encode("utf-8"), password.encode("utf-8")
|
||||||
|
):
|
||||||
raise InvalidAuthError
|
raise InvalidAuthError
|
||||||
|
|
||||||
async def async_get_or_create_credentials(
|
async def async_get_or_create_credentials(
|
||||||
self, flow_result: Dict[str, str]) -> Credentials:
|
self, flow_result: Dict[str, str]
|
||||||
|
) -> Credentials:
|
||||||
"""Get credentials based on the flow result."""
|
"""Get credentials based on the flow result."""
|
||||||
username = flow_result['username']
|
username = flow_result["username"]
|
||||||
|
|
||||||
for credential in await self.async_credentials():
|
for credential in await self.async_credentials():
|
||||||
if credential.data['username'] == username:
|
if credential.data["username"] == username:
|
||||||
return credential
|
return credential
|
||||||
|
|
||||||
# Create new credentials.
|
# Create new credentials.
|
||||||
return self.async_create_credentials({
|
return self.async_create_credentials({"username": username})
|
||||||
'username': username
|
|
||||||
})
|
|
||||||
|
|
||||||
async def async_user_meta_for_credentials(
|
async def async_user_meta_for_credentials(
|
||||||
self, credentials: Credentials) -> UserMeta:
|
self, credentials: Credentials
|
||||||
|
) -> UserMeta:
|
||||||
"""Return extra user metadata for credentials.
|
"""Return extra user metadata for credentials.
|
||||||
|
|
||||||
Will be used to populate info when creating a new user.
|
Will be used to populate info when creating a new user.
|
||||||
"""
|
"""
|
||||||
username = credentials.data['username']
|
username = credentials.data["username"]
|
||||||
name = None
|
name = None
|
||||||
|
|
||||||
for user in self.config['users']:
|
for user in self.config["users"]:
|
||||||
if user['username'] == username:
|
if user["username"] == username:
|
||||||
name = user.get('name')
|
name = user.get("name")
|
||||||
break
|
break
|
||||||
|
|
||||||
return UserMeta(name=name, is_active=True)
|
return UserMeta(name=name, is_active=True)
|
||||||
|
|
@ -92,29 +95,27 @@ class ExampleLoginFlow(LoginFlow):
|
||||||
"""Handler for the login flow."""
|
"""Handler for the login flow."""
|
||||||
|
|
||||||
async def async_step_init(
|
async def async_step_init(
|
||||||
self, user_input: Optional[Dict[str, str]] = None) \
|
self, user_input: Optional[Dict[str, str]] = None
|
||||||
-> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Handle the step of the form."""
|
"""Handle the step of the form."""
|
||||||
errors = {}
|
errors = {}
|
||||||
|
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
try:
|
try:
|
||||||
cast(ExampleAuthProvider, self._auth_provider)\
|
cast(ExampleAuthProvider, self._auth_provider).async_validate_login(
|
||||||
.async_validate_login(user_input['username'],
|
user_input["username"], user_input["password"]
|
||||||
user_input['password'])
|
)
|
||||||
except InvalidAuthError:
|
except InvalidAuthError:
|
||||||
errors['base'] = 'invalid_auth'
|
errors["base"] = "invalid_auth"
|
||||||
|
|
||||||
if not errors:
|
if not errors:
|
||||||
user_input.pop('password')
|
user_input.pop("password")
|
||||||
return await self.async_finish(user_input)
|
return await self.async_finish(user_input)
|
||||||
|
|
||||||
schema = OrderedDict() # type: Dict[str, type]
|
schema = OrderedDict() # type: Dict[str, type]
|
||||||
schema['username'] = str
|
schema["username"] = str
|
||||||
schema['password'] = str
|
schema["password"] = str
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id='init',
|
step_id="init", data_schema=vol.Schema(schema), errors=errors
|
||||||
data_schema=vol.Schema(schema),
|
|
||||||
errors=errors,
|
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -16,26 +16,23 @@ from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, LoginFlow
|
||||||
from ..models import Credentials, UserMeta
|
from ..models import Credentials, UserMeta
|
||||||
|
|
||||||
|
|
||||||
USER_SCHEMA = vol.Schema({
|
USER_SCHEMA = vol.Schema({vol.Required("username"): str})
|
||||||
vol.Required('username'): str,
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend({
|
CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend({}, extra=vol.PREVENT_EXTRA)
|
||||||
}, extra=vol.PREVENT_EXTRA)
|
|
||||||
|
|
||||||
LEGACY_USER_NAME = 'Legacy API password user'
|
LEGACY_USER_NAME = "Legacy API password user"
|
||||||
|
|
||||||
|
|
||||||
class InvalidAuthError(HomeAssistantError):
|
class InvalidAuthError(HomeAssistantError):
|
||||||
"""Raised when submitting invalid authentication."""
|
"""Raised when submitting invalid authentication."""
|
||||||
|
|
||||||
|
|
||||||
@AUTH_PROVIDERS.register('legacy_api_password')
|
@AUTH_PROVIDERS.register("legacy_api_password")
|
||||||
class LegacyApiPasswordAuthProvider(AuthProvider):
|
class LegacyApiPasswordAuthProvider(AuthProvider):
|
||||||
"""Example auth provider based on hardcoded usernames and passwords."""
|
"""Example auth provider based on hardcoded usernames and passwords."""
|
||||||
|
|
||||||
DEFAULT_TITLE = 'Legacy API Password'
|
DEFAULT_TITLE = "Legacy API Password"
|
||||||
|
|
||||||
async def async_login_flow(self, context: Optional[Dict]) -> LoginFlow:
|
async def async_login_flow(self, context: Optional[Dict]) -> LoginFlow:
|
||||||
"""Return a flow to login."""
|
"""Return a flow to login."""
|
||||||
|
|
@ -44,14 +41,16 @@ class LegacyApiPasswordAuthProvider(AuthProvider):
|
||||||
@callback
|
@callback
|
||||||
def async_validate_login(self, password: str) -> None:
|
def async_validate_login(self, password: str) -> None:
|
||||||
"""Validate a username and password."""
|
"""Validate a username and password."""
|
||||||
hass_http = getattr(self.hass, 'http', None) # type: HomeAssistantHTTP
|
hass_http = getattr(self.hass, "http", None) # type: HomeAssistantHTTP
|
||||||
|
|
||||||
if not hmac.compare_digest(hass_http.api_password.encode('utf-8'),
|
if not hmac.compare_digest(
|
||||||
password.encode('utf-8')):
|
hass_http.api_password.encode("utf-8"), password.encode("utf-8")
|
||||||
|
):
|
||||||
raise InvalidAuthError
|
raise InvalidAuthError
|
||||||
|
|
||||||
async def async_get_or_create_credentials(
|
async def async_get_or_create_credentials(
|
||||||
self, flow_result: Dict[str, str]) -> Credentials:
|
self, flow_result: Dict[str, str]
|
||||||
|
) -> Credentials:
|
||||||
"""Return credentials for this login."""
|
"""Return credentials for this login."""
|
||||||
credentials = await self.async_credentials()
|
credentials = await self.async_credentials()
|
||||||
if credentials:
|
if credentials:
|
||||||
|
|
@ -60,7 +59,8 @@ class LegacyApiPasswordAuthProvider(AuthProvider):
|
||||||
return self.async_create_credentials({})
|
return self.async_create_credentials({})
|
||||||
|
|
||||||
async def async_user_meta_for_credentials(
|
async def async_user_meta_for_credentials(
|
||||||
self, credentials: Credentials) -> UserMeta:
|
self, credentials: Credentials
|
||||||
|
) -> UserMeta:
|
||||||
"""
|
"""
|
||||||
Return info for the user.
|
Return info for the user.
|
||||||
|
|
||||||
|
|
@ -73,29 +73,26 @@ class LegacyLoginFlow(LoginFlow):
|
||||||
"""Handler for the login flow."""
|
"""Handler for the login flow."""
|
||||||
|
|
||||||
async def async_step_init(
|
async def async_step_init(
|
||||||
self, user_input: Optional[Dict[str, str]] = None) \
|
self, user_input: Optional[Dict[str, str]] = None
|
||||||
-> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Handle the step of the form."""
|
"""Handle the step of the form."""
|
||||||
errors = {}
|
errors = {}
|
||||||
|
|
||||||
hass_http = getattr(self.hass, 'http', None)
|
hass_http = getattr(self.hass, "http", None)
|
||||||
if hass_http is None or not hass_http.api_password:
|
if hass_http is None or not hass_http.api_password:
|
||||||
return self.async_abort(
|
return self.async_abort(reason="no_api_password_set")
|
||||||
reason='no_api_password_set'
|
|
||||||
)
|
|
||||||
|
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
try:
|
try:
|
||||||
cast(LegacyApiPasswordAuthProvider, self._auth_provider)\
|
cast(
|
||||||
.async_validate_login(user_input['password'])
|
LegacyApiPasswordAuthProvider, self._auth_provider
|
||||||
|
).async_validate_login(user_input["password"])
|
||||||
except InvalidAuthError:
|
except InvalidAuthError:
|
||||||
errors['base'] = 'invalid_auth'
|
errors["base"] = "invalid_auth"
|
||||||
|
|
||||||
if not errors:
|
if not errors:
|
||||||
return await self.async_finish({})
|
return await self.async_finish({})
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id='init',
|
step_id="init", data_schema=vol.Schema({"password": str}), errors=errors
|
||||||
data_schema=vol.Schema({'password': str}),
|
|
||||||
errors=errors,
|
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,7 @@ from homeassistant.exceptions import HomeAssistantError
|
||||||
from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, LoginFlow
|
from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, LoginFlow
|
||||||
from ..models import Credentials, UserMeta
|
from ..models import Credentials, UserMeta
|
||||||
|
|
||||||
CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend({
|
CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend({}, extra=vol.PREVENT_EXTRA)
|
||||||
}, extra=vol.PREVENT_EXTRA)
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidAuthError(HomeAssistantError):
|
class InvalidAuthError(HomeAssistantError):
|
||||||
|
|
@ -26,14 +25,14 @@ class InvalidUserError(HomeAssistantError):
|
||||||
"""Raised when try to login as invalid user."""
|
"""Raised when try to login as invalid user."""
|
||||||
|
|
||||||
|
|
||||||
@AUTH_PROVIDERS.register('trusted_networks')
|
@AUTH_PROVIDERS.register("trusted_networks")
|
||||||
class TrustedNetworksAuthProvider(AuthProvider):
|
class TrustedNetworksAuthProvider(AuthProvider):
|
||||||
"""Trusted Networks auth provider.
|
"""Trusted Networks auth provider.
|
||||||
|
|
||||||
Allow passwordless access from trusted network.
|
Allow passwordless access from trusted network.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
DEFAULT_TITLE = 'Trusted Networks'
|
DEFAULT_TITLE = "Trusted Networks"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def support_mfa(self) -> bool:
|
def support_mfa(self) -> bool:
|
||||||
|
|
@ -44,27 +43,29 @@ class TrustedNetworksAuthProvider(AuthProvider):
|
||||||
"""Return a flow to login."""
|
"""Return a flow to login."""
|
||||||
assert context is not None
|
assert context is not None
|
||||||
users = await self.store.async_get_users()
|
users = await self.store.async_get_users()
|
||||||
available_users = {user.id: user.name
|
available_users = {
|
||||||
for user in users
|
user.id: user.name
|
||||||
if not user.system_generated and user.is_active}
|
for user in users
|
||||||
|
if not user.system_generated and user.is_active
|
||||||
|
}
|
||||||
|
|
||||||
return TrustedNetworksLoginFlow(
|
return TrustedNetworksLoginFlow(
|
||||||
self, cast(str, context.get('ip_address')), available_users)
|
self, cast(str, context.get("ip_address")), available_users
|
||||||
|
)
|
||||||
|
|
||||||
async def async_get_or_create_credentials(
|
async def async_get_or_create_credentials(
|
||||||
self, flow_result: Dict[str, str]) -> Credentials:
|
self, flow_result: Dict[str, str]
|
||||||
|
) -> Credentials:
|
||||||
"""Get credentials based on the flow result."""
|
"""Get credentials based on the flow result."""
|
||||||
user_id = flow_result['user']
|
user_id = flow_result["user"]
|
||||||
|
|
||||||
users = await self.store.async_get_users()
|
users = await self.store.async_get_users()
|
||||||
for user in users:
|
for user in users:
|
||||||
if (not user.system_generated and
|
if not user.system_generated and user.is_active and user.id == user_id:
|
||||||
user.is_active and
|
|
||||||
user.id == user_id):
|
|
||||||
for credential in await self.async_credentials():
|
for credential in await self.async_credentials():
|
||||||
if credential.data['user_id'] == user_id:
|
if credential.data["user_id"] == user_id:
|
||||||
return credential
|
return credential
|
||||||
cred = self.async_create_credentials({'user_id': user_id})
|
cred = self.async_create_credentials({"user_id": user_id})
|
||||||
await self.store.async_link_user(user, cred)
|
await self.store.async_link_user(user, cred)
|
||||||
return cred
|
return cred
|
||||||
|
|
||||||
|
|
@ -72,7 +73,8 @@ class TrustedNetworksAuthProvider(AuthProvider):
|
||||||
raise InvalidUserError
|
raise InvalidUserError
|
||||||
|
|
||||||
async def async_user_meta_for_credentials(
|
async def async_user_meta_for_credentials(
|
||||||
self, credentials: Credentials) -> UserMeta:
|
self, credentials: Credentials
|
||||||
|
) -> UserMeta:
|
||||||
"""Return extra user metadata for credentials.
|
"""Return extra user metadata for credentials.
|
||||||
|
|
||||||
Trusted network auth provider should never create new user.
|
Trusted network auth provider should never create new user.
|
||||||
|
|
@ -86,44 +88,48 @@ class TrustedNetworksAuthProvider(AuthProvider):
|
||||||
Raise InvalidAuthError if not.
|
Raise InvalidAuthError if not.
|
||||||
Raise InvalidAuthError if trusted_networks is not configured.
|
Raise InvalidAuthError if trusted_networks is not configured.
|
||||||
"""
|
"""
|
||||||
hass_http = getattr(self.hass, 'http', None) # type: HomeAssistantHTTP
|
hass_http = getattr(self.hass, "http", None) # type: HomeAssistantHTTP
|
||||||
|
|
||||||
if not hass_http or not hass_http.trusted_networks:
|
if not hass_http or not hass_http.trusted_networks:
|
||||||
raise InvalidAuthError('trusted_networks is not configured')
|
raise InvalidAuthError("trusted_networks is not configured")
|
||||||
|
|
||||||
if not any(ip_address in trusted_network for trusted_network
|
if not any(
|
||||||
in hass_http.trusted_networks):
|
ip_address in trusted_network
|
||||||
raise InvalidAuthError('Not in trusted_networks')
|
for trusted_network in hass_http.trusted_networks
|
||||||
|
):
|
||||||
|
raise InvalidAuthError("Not in trusted_networks")
|
||||||
|
|
||||||
|
|
||||||
class TrustedNetworksLoginFlow(LoginFlow):
|
class TrustedNetworksLoginFlow(LoginFlow):
|
||||||
"""Handler for the login flow."""
|
"""Handler for the login flow."""
|
||||||
|
|
||||||
def __init__(self, auth_provider: TrustedNetworksAuthProvider,
|
def __init__(
|
||||||
ip_address: str, available_users: Dict[str, Optional[str]]) \
|
self,
|
||||||
-> None:
|
auth_provider: TrustedNetworksAuthProvider,
|
||||||
|
ip_address: str,
|
||||||
|
available_users: Dict[str, Optional[str]],
|
||||||
|
) -> None:
|
||||||
"""Initialize the login flow."""
|
"""Initialize the login flow."""
|
||||||
super().__init__(auth_provider)
|
super().__init__(auth_provider)
|
||||||
self._available_users = available_users
|
self._available_users = available_users
|
||||||
self._ip_address = ip_address
|
self._ip_address = ip_address
|
||||||
|
|
||||||
async def async_step_init(
|
async def async_step_init(
|
||||||
self, user_input: Optional[Dict[str, str]] = None) \
|
self, user_input: Optional[Dict[str, str]] = None
|
||||||
-> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Handle the step of the form."""
|
"""Handle the step of the form."""
|
||||||
try:
|
try:
|
||||||
cast(TrustedNetworksAuthProvider, self._auth_provider)\
|
cast(
|
||||||
.async_validate_access(self._ip_address)
|
TrustedNetworksAuthProvider, self._auth_provider
|
||||||
|
).async_validate_access(self._ip_address)
|
||||||
|
|
||||||
except InvalidAuthError:
|
except InvalidAuthError:
|
||||||
return self.async_abort(
|
return self.async_abort(reason="not_whitelisted")
|
||||||
reason='not_whitelisted'
|
|
||||||
)
|
|
||||||
|
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
return await self.async_finish(user_input)
|
return await self.async_finish(user_input)
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id='init',
|
step_id="init",
|
||||||
data_schema=vol.Schema({'user': vol.In(self._available_users)}),
|
data_schema=vol.Schema({"user": vol.In(self._available_users)}),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -10,4 +10,4 @@ def generate_secret(entropy: int = 32) -> str:
|
||||||
|
|
||||||
Event loop friendly.
|
Event loop friendly.
|
||||||
"""
|
"""
|
||||||
return binascii.hexlify(os.urandom(entropy)).decode('ascii')
|
return binascii.hexlify(os.urandom(entropy)).decode("ascii")
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,11 @@ from typing import Any, Optional, Dict
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant import (
|
from homeassistant import (
|
||||||
core, config as conf_util, config_entries, components as core_components)
|
core,
|
||||||
|
config as conf_util,
|
||||||
|
config_entries,
|
||||||
|
components as core_components,
|
||||||
|
)
|
||||||
from homeassistant.components import persistent_notification
|
from homeassistant.components import persistent_notification
|
||||||
from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE
|
from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
@ -22,25 +26,34 @@ from homeassistant.helpers.signal import async_register_signal_handling
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
ERROR_LOG_FILENAME = 'home-assistant.log'
|
ERROR_LOG_FILENAME = "home-assistant.log"
|
||||||
|
|
||||||
# hass.data key for logging information.
|
# hass.data key for logging information.
|
||||||
DATA_LOGGING = 'logging'
|
DATA_LOGGING = "logging"
|
||||||
|
|
||||||
FIRST_INIT_COMPONENT = {'system_log', 'recorder', 'mqtt', 'mqtt_eventstream',
|
FIRST_INIT_COMPONENT = {
|
||||||
'logger', 'introduction', 'frontend', 'history'}
|
"system_log",
|
||||||
|
"recorder",
|
||||||
|
"mqtt",
|
||||||
|
"mqtt_eventstream",
|
||||||
|
"logger",
|
||||||
|
"introduction",
|
||||||
|
"frontend",
|
||||||
|
"history",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def from_config_dict(config: Dict[str, Any],
|
def from_config_dict(
|
||||||
hass: Optional[core.HomeAssistant] = None,
|
config: Dict[str, Any],
|
||||||
config_dir: Optional[str] = None,
|
hass: Optional[core.HomeAssistant] = None,
|
||||||
enable_log: bool = True,
|
config_dir: Optional[str] = None,
|
||||||
verbose: bool = False,
|
enable_log: bool = True,
|
||||||
skip_pip: bool = False,
|
verbose: bool = False,
|
||||||
log_rotate_days: Any = None,
|
skip_pip: bool = False,
|
||||||
log_file: Any = None,
|
log_rotate_days: Any = None,
|
||||||
log_no_color: bool = False) \
|
log_file: Any = None,
|
||||||
-> Optional[core.HomeAssistant]:
|
log_no_color: bool = False,
|
||||||
|
) -> Optional[core.HomeAssistant]:
|
||||||
"""Try to configure Home Assistant from a configuration dictionary.
|
"""Try to configure Home Assistant from a configuration dictionary.
|
||||||
|
|
||||||
Dynamically loads required components and its dependencies.
|
Dynamically loads required components and its dependencies.
|
||||||
|
|
@ -51,28 +64,36 @@ def from_config_dict(config: Dict[str, Any],
|
||||||
config_dir = os.path.abspath(config_dir)
|
config_dir = os.path.abspath(config_dir)
|
||||||
hass.config.config_dir = config_dir
|
hass.config.config_dir = config_dir
|
||||||
if not is_virtual_env():
|
if not is_virtual_env():
|
||||||
hass.loop.run_until_complete(
|
hass.loop.run_until_complete(async_mount_local_lib_path(config_dir))
|
||||||
async_mount_local_lib_path(config_dir))
|
|
||||||
|
|
||||||
# run task
|
# run task
|
||||||
hass = hass.loop.run_until_complete(
|
hass = hass.loop.run_until_complete(
|
||||||
async_from_config_dict(
|
async_from_config_dict(
|
||||||
config, hass, config_dir, enable_log, verbose, skip_pip,
|
config,
|
||||||
log_rotate_days, log_file, log_no_color)
|
hass,
|
||||||
|
config_dir,
|
||||||
|
enable_log,
|
||||||
|
verbose,
|
||||||
|
skip_pip,
|
||||||
|
log_rotate_days,
|
||||||
|
log_file,
|
||||||
|
log_no_color,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
return hass
|
return hass
|
||||||
|
|
||||||
|
|
||||||
async def async_from_config_dict(config: Dict[str, Any],
|
async def async_from_config_dict(
|
||||||
hass: core.HomeAssistant,
|
config: Dict[str, Any],
|
||||||
config_dir: Optional[str] = None,
|
hass: core.HomeAssistant,
|
||||||
enable_log: bool = True,
|
config_dir: Optional[str] = None,
|
||||||
verbose: bool = False,
|
enable_log: bool = True,
|
||||||
skip_pip: bool = False,
|
verbose: bool = False,
|
||||||
log_rotate_days: Any = None,
|
skip_pip: bool = False,
|
||||||
log_file: Any = None,
|
log_rotate_days: Any = None,
|
||||||
log_no_color: bool = False) \
|
log_file: Any = None,
|
||||||
-> Optional[core.HomeAssistant]:
|
log_no_color: bool = False,
|
||||||
|
) -> Optional[core.HomeAssistant]:
|
||||||
"""Try to configure Home Assistant from a configuration dictionary.
|
"""Try to configure Home Assistant from a configuration dictionary.
|
||||||
|
|
||||||
Dynamically loads required components and its dependencies.
|
Dynamically loads required components and its dependencies.
|
||||||
|
|
@ -81,40 +102,41 @@ async def async_from_config_dict(config: Dict[str, Any],
|
||||||
start = time()
|
start = time()
|
||||||
|
|
||||||
if enable_log:
|
if enable_log:
|
||||||
async_enable_logging(hass, verbose, log_rotate_days, log_file,
|
async_enable_logging(hass, verbose, log_rotate_days, log_file, log_no_color)
|
||||||
log_no_color)
|
|
||||||
|
|
||||||
core_config = config.get(core.DOMAIN, {})
|
core_config = config.get(core.DOMAIN, {})
|
||||||
has_api_password = bool((config.get('http') or {}).get('api_password'))
|
has_api_password = bool((config.get("http") or {}).get("api_password"))
|
||||||
has_trusted_networks = bool((config.get('http') or {})
|
has_trusted_networks = bool((config.get("http") or {}).get("trusted_networks"))
|
||||||
.get('trusted_networks'))
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await conf_util.async_process_ha_core_config(
|
await conf_util.async_process_ha_core_config(
|
||||||
hass, core_config, has_api_password, has_trusted_networks)
|
hass, core_config, has_api_password, has_trusted_networks
|
||||||
|
)
|
||||||
except vol.Invalid as config_err:
|
except vol.Invalid as config_err:
|
||||||
conf_util.async_log_exception(
|
conf_util.async_log_exception(config_err, "homeassistant", core_config, hass)
|
||||||
config_err, 'homeassistant', core_config, hass)
|
|
||||||
return None
|
return None
|
||||||
except HomeAssistantError:
|
except HomeAssistantError:
|
||||||
_LOGGER.error("Home Assistant core failed to initialize. "
|
_LOGGER.error(
|
||||||
"Further initialization aborted")
|
"Home Assistant core failed to initialize. "
|
||||||
|
"Further initialization aborted"
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
await hass.async_add_executor_job(
|
await hass.async_add_executor_job(conf_util.process_ha_config_upgrade, hass)
|
||||||
conf_util.process_ha_config_upgrade, hass)
|
|
||||||
|
|
||||||
hass.config.skip_pip = skip_pip
|
hass.config.skip_pip = skip_pip
|
||||||
if skip_pip:
|
if skip_pip:
|
||||||
_LOGGER.warning("Skipping pip installation of required modules. "
|
_LOGGER.warning(
|
||||||
"This may cause issues")
|
"Skipping pip installation of required modules. " "This may cause issues"
|
||||||
|
)
|
||||||
|
|
||||||
# Make a copy because we are mutating it.
|
# Make a copy because we are mutating it.
|
||||||
config = OrderedDict(config)
|
config = OrderedDict(config)
|
||||||
|
|
||||||
# Merge packages
|
# Merge packages
|
||||||
conf_util.merge_packages_config(
|
conf_util.merge_packages_config(
|
||||||
hass, config, core_config.get(conf_util.CONF_PACKAGES, {}))
|
hass, config, core_config.get(conf_util.CONF_PACKAGES, {})
|
||||||
|
)
|
||||||
|
|
||||||
# Ensure we have no None values after merge
|
# Ensure we have no None values after merge
|
||||||
for key, value in config.items():
|
for key, value in config.items():
|
||||||
|
|
@ -125,15 +147,16 @@ async def async_from_config_dict(config: Dict[str, Any],
|
||||||
await hass.config_entries.async_load()
|
await hass.config_entries.async_load()
|
||||||
|
|
||||||
# Filter out the repeating and common config section [homeassistant]
|
# Filter out the repeating and common config section [homeassistant]
|
||||||
components = set(key.split(' ')[0] for key in config.keys()
|
components = set(key.split(" ")[0] for key in config.keys() if key != core.DOMAIN)
|
||||||
if key != core.DOMAIN)
|
|
||||||
components.update(hass.config_entries.async_domains())
|
components.update(hass.config_entries.async_domains())
|
||||||
|
|
||||||
# setup components
|
# setup components
|
||||||
res = await core_components.async_setup(hass, config)
|
res = await core_components.async_setup(hass, config)
|
||||||
if not res:
|
if not res:
|
||||||
_LOGGER.error("Home Assistant core failed to initialize. "
|
_LOGGER.error(
|
||||||
"Further initialization aborted")
|
"Home Assistant core failed to initialize. "
|
||||||
|
"Further initialization aborted"
|
||||||
|
)
|
||||||
return hass
|
return hass
|
||||||
|
|
||||||
await persistent_notification.async_setup(hass, config)
|
await persistent_notification.async_setup(hass, config)
|
||||||
|
|
@ -157,20 +180,21 @@ async def async_from_config_dict(config: Dict[str, Any],
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
stop = time()
|
stop = time()
|
||||||
_LOGGER.info("Home Assistant initialized in %.2fs", stop-start)
|
_LOGGER.info("Home Assistant initialized in %.2fs", stop - start)
|
||||||
|
|
||||||
async_register_signal_handling(hass)
|
async_register_signal_handling(hass)
|
||||||
return hass
|
return hass
|
||||||
|
|
||||||
|
|
||||||
def from_config_file(config_path: str,
|
def from_config_file(
|
||||||
hass: Optional[core.HomeAssistant] = None,
|
config_path: str,
|
||||||
verbose: bool = False,
|
hass: Optional[core.HomeAssistant] = None,
|
||||||
skip_pip: bool = True,
|
verbose: bool = False,
|
||||||
log_rotate_days: Any = None,
|
skip_pip: bool = True,
|
||||||
log_file: Any = None,
|
log_rotate_days: Any = None,
|
||||||
log_no_color: bool = False)\
|
log_file: Any = None,
|
||||||
-> Optional[core.HomeAssistant]:
|
log_no_color: bool = False,
|
||||||
|
) -> Optional[core.HomeAssistant]:
|
||||||
"""Read the configuration file and try to start all the functionality.
|
"""Read the configuration file and try to start all the functionality.
|
||||||
|
|
||||||
Will add functionality to 'hass' parameter if given,
|
Will add functionality to 'hass' parameter if given,
|
||||||
|
|
@ -182,21 +206,28 @@ def from_config_file(config_path: str,
|
||||||
# run task
|
# run task
|
||||||
hass = hass.loop.run_until_complete(
|
hass = hass.loop.run_until_complete(
|
||||||
async_from_config_file(
|
async_from_config_file(
|
||||||
config_path, hass, verbose, skip_pip,
|
config_path,
|
||||||
log_rotate_days, log_file, log_no_color)
|
hass,
|
||||||
|
verbose,
|
||||||
|
skip_pip,
|
||||||
|
log_rotate_days,
|
||||||
|
log_file,
|
||||||
|
log_no_color,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
return hass
|
return hass
|
||||||
|
|
||||||
|
|
||||||
async def async_from_config_file(config_path: str,
|
async def async_from_config_file(
|
||||||
hass: core.HomeAssistant,
|
config_path: str,
|
||||||
verbose: bool = False,
|
hass: core.HomeAssistant,
|
||||||
skip_pip: bool = True,
|
verbose: bool = False,
|
||||||
log_rotate_days: Any = None,
|
skip_pip: bool = True,
|
||||||
log_file: Any = None,
|
log_rotate_days: Any = None,
|
||||||
log_no_color: bool = False)\
|
log_file: Any = None,
|
||||||
-> Optional[core.HomeAssistant]:
|
log_no_color: bool = False,
|
||||||
|
) -> Optional[core.HomeAssistant]:
|
||||||
"""Read the configuration file and try to start all the functionality.
|
"""Read the configuration file and try to start all the functionality.
|
||||||
|
|
||||||
Will add functionality to 'hass' parameter.
|
Will add functionality to 'hass' parameter.
|
||||||
|
|
@ -209,12 +240,12 @@ async def async_from_config_file(config_path: str,
|
||||||
if not is_virtual_env():
|
if not is_virtual_env():
|
||||||
await async_mount_local_lib_path(config_dir)
|
await async_mount_local_lib_path(config_dir)
|
||||||
|
|
||||||
async_enable_logging(hass, verbose, log_rotate_days, log_file,
|
async_enable_logging(hass, verbose, log_rotate_days, log_file, log_no_color)
|
||||||
log_no_color)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
config_dict = await hass.async_add_executor_job(
|
config_dict = await hass.async_add_executor_job(
|
||||||
conf_util.load_yaml_config_file, config_path)
|
conf_util.load_yaml_config_file, config_path
|
||||||
|
)
|
||||||
except HomeAssistantError as err:
|
except HomeAssistantError as err:
|
||||||
_LOGGER.error("Error loading %s: %s", config_path, err)
|
_LOGGER.error("Error loading %s: %s", config_path, err)
|
||||||
return None
|
return None
|
||||||
|
|
@ -222,43 +253,48 @@ async def async_from_config_file(config_path: str,
|
||||||
clear_secret_cache()
|
clear_secret_cache()
|
||||||
|
|
||||||
return await async_from_config_dict(
|
return await async_from_config_dict(
|
||||||
config_dict, hass, enable_log=False, skip_pip=skip_pip)
|
config_dict, hass, enable_log=False, skip_pip=skip_pip
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@core.callback
|
@core.callback
|
||||||
def async_enable_logging(hass: core.HomeAssistant,
|
def async_enable_logging(
|
||||||
verbose: bool = False,
|
hass: core.HomeAssistant,
|
||||||
log_rotate_days: Optional[int] = None,
|
verbose: bool = False,
|
||||||
log_file: Optional[str] = None,
|
log_rotate_days: Optional[int] = None,
|
||||||
log_no_color: bool = False) -> None:
|
log_file: Optional[str] = None,
|
||||||
|
log_no_color: bool = False,
|
||||||
|
) -> None:
|
||||||
"""Set up the logging.
|
"""Set up the logging.
|
||||||
|
|
||||||
This method must be run in the event loop.
|
This method must be run in the event loop.
|
||||||
"""
|
"""
|
||||||
fmt = ("%(asctime)s %(levelname)s (%(threadName)s) "
|
fmt = "%(asctime)s %(levelname)s (%(threadName)s) " "[%(name)s] %(message)s"
|
||||||
"[%(name)s] %(message)s")
|
datefmt = "%Y-%m-%d %H:%M:%S"
|
||||||
datefmt = '%Y-%m-%d %H:%M:%S'
|
|
||||||
|
|
||||||
if not log_no_color:
|
if not log_no_color:
|
||||||
try:
|
try:
|
||||||
from colorlog import ColoredFormatter
|
from colorlog import ColoredFormatter
|
||||||
|
|
||||||
# basicConfig must be called after importing colorlog in order to
|
# basicConfig must be called after importing colorlog in order to
|
||||||
# ensure that the handlers it sets up wraps the correct streams.
|
# ensure that the handlers it sets up wraps the correct streams.
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
||||||
colorfmt = "%(log_color)s{}%(reset)s".format(fmt)
|
colorfmt = "%(log_color)s{}%(reset)s".format(fmt)
|
||||||
logging.getLogger().handlers[0].setFormatter(ColoredFormatter(
|
logging.getLogger().handlers[0].setFormatter(
|
||||||
colorfmt,
|
ColoredFormatter(
|
||||||
datefmt=datefmt,
|
colorfmt,
|
||||||
reset=True,
|
datefmt=datefmt,
|
||||||
log_colors={
|
reset=True,
|
||||||
'DEBUG': 'cyan',
|
log_colors={
|
||||||
'INFO': 'green',
|
"DEBUG": "cyan",
|
||||||
'WARNING': 'yellow',
|
"INFO": "green",
|
||||||
'ERROR': 'red',
|
"WARNING": "yellow",
|
||||||
'CRITICAL': 'red',
|
"ERROR": "red",
|
||||||
}
|
"CRITICAL": "red",
|
||||||
))
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
@ -267,9 +303,9 @@ def async_enable_logging(hass: core.HomeAssistant,
|
||||||
logging.basicConfig(format=fmt, datefmt=datefmt, level=logging.INFO)
|
logging.basicConfig(format=fmt, datefmt=datefmt, level=logging.INFO)
|
||||||
|
|
||||||
# Suppress overly verbose logs from libraries that aren't helpful
|
# Suppress overly verbose logs from libraries that aren't helpful
|
||||||
logging.getLogger('requests').setLevel(logging.WARNING)
|
logging.getLogger("requests").setLevel(logging.WARNING)
|
||||||
logging.getLogger('urllib3').setLevel(logging.WARNING)
|
logging.getLogger("urllib3").setLevel(logging.WARNING)
|
||||||
logging.getLogger('aiohttp.access').setLevel(logging.WARNING)
|
logging.getLogger("aiohttp.access").setLevel(logging.WARNING)
|
||||||
|
|
||||||
# Log errors to a file if we have write access to file or config dir
|
# Log errors to a file if we have write access to file or config dir
|
||||||
if log_file is None:
|
if log_file is None:
|
||||||
|
|
@ -282,16 +318,16 @@ def async_enable_logging(hass: core.HomeAssistant,
|
||||||
|
|
||||||
# Check if we can write to the error log if it exists or that
|
# Check if we can write to the error log if it exists or that
|
||||||
# we can create files in the containing directory if not.
|
# we can create files in the containing directory if not.
|
||||||
if (err_path_exists and os.access(err_log_path, os.W_OK)) or \
|
if (err_path_exists and os.access(err_log_path, os.W_OK)) or (
|
||||||
(not err_path_exists and os.access(err_dir, os.W_OK)):
|
not err_path_exists and os.access(err_dir, os.W_OK)
|
||||||
|
):
|
||||||
|
|
||||||
if log_rotate_days:
|
if log_rotate_days:
|
||||||
err_handler = logging.handlers.TimedRotatingFileHandler(
|
err_handler = logging.handlers.TimedRotatingFileHandler(
|
||||||
err_log_path, when='midnight',
|
err_log_path, when="midnight", backupCount=log_rotate_days
|
||||||
backupCount=log_rotate_days) # type: logging.FileHandler
|
) # type: logging.FileHandler
|
||||||
else:
|
else:
|
||||||
err_handler = logging.FileHandler(
|
err_handler = logging.FileHandler(err_log_path, mode="w", delay=True)
|
||||||
err_log_path, mode='w', delay=True)
|
|
||||||
|
|
||||||
err_handler.setLevel(logging.INFO if verbose else logging.WARNING)
|
err_handler.setLevel(logging.INFO if verbose else logging.WARNING)
|
||||||
err_handler.setFormatter(logging.Formatter(fmt, datefmt=datefmt))
|
err_handler.setFormatter(logging.Formatter(fmt, datefmt=datefmt))
|
||||||
|
|
@ -300,21 +336,19 @@ def async_enable_logging(hass: core.HomeAssistant,
|
||||||
|
|
||||||
async def async_stop_async_handler(_: Any) -> None:
|
async def async_stop_async_handler(_: Any) -> None:
|
||||||
"""Cleanup async handler."""
|
"""Cleanup async handler."""
|
||||||
logging.getLogger('').removeHandler(async_handler) # type: ignore
|
logging.getLogger("").removeHandler(async_handler) # type: ignore
|
||||||
await async_handler.async_close(blocking=True)
|
await async_handler.async_close(blocking=True)
|
||||||
|
|
||||||
hass.bus.async_listen_once(
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_CLOSE, async_stop_async_handler)
|
||||||
EVENT_HOMEASSISTANT_CLOSE, async_stop_async_handler)
|
|
||||||
|
|
||||||
logger = logging.getLogger('')
|
logger = logging.getLogger("")
|
||||||
logger.addHandler(async_handler) # type: ignore
|
logger.addHandler(async_handler) # type: ignore
|
||||||
logger.setLevel(logging.INFO)
|
logger.setLevel(logging.INFO)
|
||||||
|
|
||||||
# Save the log file location for access by other components.
|
# Save the log file location for access by other components.
|
||||||
hass.data[DATA_LOGGING] = err_log_path
|
hass.data[DATA_LOGGING] = err_log_path
|
||||||
else:
|
else:
|
||||||
_LOGGER.error(
|
_LOGGER.error("Unable to set up error log %s (access denied)", err_log_path)
|
||||||
"Unable to set up error log %s (access denied)", err_log_path)
|
|
||||||
|
|
||||||
|
|
||||||
async def async_mount_local_lib_path(config_dir: str) -> str:
|
async def async_mount_local_lib_path(config_dir: str) -> str:
|
||||||
|
|
@ -322,7 +356,7 @@ async def async_mount_local_lib_path(config_dir: str) -> str:
|
||||||
|
|
||||||
This function is a coroutine.
|
This function is a coroutine.
|
||||||
"""
|
"""
|
||||||
deps_dir = os.path.join(config_dir, 'deps')
|
deps_dir = os.path.join(config_dir, "deps")
|
||||||
lib_dir = await async_get_user_site(deps_dir)
|
lib_dir = await async_get_user_site(deps_dir)
|
||||||
if lib_dir not in sys.path:
|
if lib_dir not in sys.path:
|
||||||
sys.path.insert(0, lib_dir)
|
sys.path.insert(0, lib_dir)
|
||||||
|
|
|
||||||
|
|
@ -18,14 +18,19 @@ from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers.service import extract_entity_ids
|
from homeassistant.helpers.service import extract_entity_ids
|
||||||
from homeassistant.helpers import intent
|
from homeassistant.helpers import intent
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE,
|
ATTR_ENTITY_ID,
|
||||||
SERVICE_HOMEASSISTANT_STOP, SERVICE_HOMEASSISTANT_RESTART,
|
SERVICE_TURN_ON,
|
||||||
RESTART_EXIT_CODE)
|
SERVICE_TURN_OFF,
|
||||||
|
SERVICE_TOGGLE,
|
||||||
|
SERVICE_HOMEASSISTANT_STOP,
|
||||||
|
SERVICE_HOMEASSISTANT_RESTART,
|
||||||
|
RESTART_EXIT_CODE,
|
||||||
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
SERVICE_RELOAD_CORE_CONFIG = 'reload_core_config'
|
SERVICE_RELOAD_CORE_CONFIG = "reload_core_config"
|
||||||
SERVICE_CHECK_CONFIG = 'check_config'
|
SERVICE_CHECK_CONFIG = "check_config"
|
||||||
|
|
||||||
|
|
||||||
def is_on(hass, entity_id=None):
|
def is_on(hass, entity_id=None):
|
||||||
|
|
@ -45,11 +50,10 @@ def is_on(hass, entity_id=None):
|
||||||
component = getattr(hass.components, domain)
|
component = getattr(hass.components, domain)
|
||||||
|
|
||||||
except ImportError:
|
except ImportError:
|
||||||
_LOGGER.error('Failed to call %s.is_on: component not found',
|
_LOGGER.error("Failed to call %s.is_on: component not found", domain)
|
||||||
domain)
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if not hasattr(component, 'is_on'):
|
if not hasattr(component, "is_on"):
|
||||||
_LOGGER.warning("Component %s has no is_on method.", domain)
|
_LOGGER.warning("Component %s has no is_on method.", domain)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
@ -112,6 +116,7 @@ def async_reload_core_config(hass):
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]:
|
def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]:
|
||||||
"""Set up general services related to Home Assistant."""
|
"""Set up general services related to Home Assistant."""
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_handle_turn_service(service):
|
def async_handle_turn_service(service):
|
||||||
"""Handle calls to homeassistant.turn_on/off."""
|
"""Handle calls to homeassistant.turn_on/off."""
|
||||||
|
|
@ -120,13 +125,14 @@ def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]:
|
||||||
# Generic turn on/off method requires entity id
|
# Generic turn on/off method requires entity id
|
||||||
if not entity_ids:
|
if not entity_ids:
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
"homeassistant/%s cannot be called without entity_id",
|
"homeassistant/%s cannot be called without entity_id", service.service
|
||||||
service.service)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Group entity_ids by domain. groupby requires sorted data.
|
# Group entity_ids by domain. groupby requires sorted data.
|
||||||
by_domain = it.groupby(sorted(entity_ids),
|
by_domain = it.groupby(
|
||||||
lambda item: ha.split_entity_id(item)[0])
|
sorted(entity_ids), lambda item: ha.split_entity_id(item)[0]
|
||||||
|
)
|
||||||
|
|
||||||
tasks = []
|
tasks = []
|
||||||
|
|
||||||
|
|
@ -145,24 +151,30 @@ def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]:
|
||||||
# ent_ids is a generator, convert it to a list.
|
# ent_ids is a generator, convert it to a list.
|
||||||
data[ATTR_ENTITY_ID] = list(ent_ids)
|
data[ATTR_ENTITY_ID] = list(ent_ids)
|
||||||
|
|
||||||
tasks.append(hass.services.async_call(
|
tasks.append(
|
||||||
domain, service.service, data, blocking))
|
hass.services.async_call(domain, service.service, data, blocking)
|
||||||
|
)
|
||||||
|
|
||||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||||
|
|
||||||
hass.services.async_register(
|
hass.services.async_register(ha.DOMAIN, SERVICE_TURN_OFF, async_handle_turn_service)
|
||||||
ha.DOMAIN, SERVICE_TURN_OFF, async_handle_turn_service)
|
hass.services.async_register(ha.DOMAIN, SERVICE_TURN_ON, async_handle_turn_service)
|
||||||
hass.services.async_register(
|
hass.services.async_register(ha.DOMAIN, SERVICE_TOGGLE, async_handle_turn_service)
|
||||||
ha.DOMAIN, SERVICE_TURN_ON, async_handle_turn_service)
|
hass.helpers.intent.async_register(
|
||||||
hass.services.async_register(
|
intent.ServiceIntentHandler(
|
||||||
ha.DOMAIN, SERVICE_TOGGLE, async_handle_turn_service)
|
intent.INTENT_TURN_ON, ha.DOMAIN, SERVICE_TURN_ON, "Turned {} on"
|
||||||
hass.helpers.intent.async_register(intent.ServiceIntentHandler(
|
)
|
||||||
intent.INTENT_TURN_ON, ha.DOMAIN, SERVICE_TURN_ON, "Turned {} on"))
|
)
|
||||||
hass.helpers.intent.async_register(intent.ServiceIntentHandler(
|
hass.helpers.intent.async_register(
|
||||||
intent.INTENT_TURN_OFF, ha.DOMAIN, SERVICE_TURN_OFF,
|
intent.ServiceIntentHandler(
|
||||||
"Turned {} off"))
|
intent.INTENT_TURN_OFF, ha.DOMAIN, SERVICE_TURN_OFF, "Turned {} off"
|
||||||
hass.helpers.intent.async_register(intent.ServiceIntentHandler(
|
)
|
||||||
intent.INTENT_TOGGLE, ha.DOMAIN, SERVICE_TOGGLE, "Toggled {}"))
|
)
|
||||||
|
hass.helpers.intent.async_register(
|
||||||
|
intent.ServiceIntentHandler(
|
||||||
|
intent.INTENT_TOGGLE, ha.DOMAIN, SERVICE_TOGGLE, "Toggled {}"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_handle_core_service(call):
|
def async_handle_core_service(call):
|
||||||
|
|
@ -180,18 +192,23 @@ def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]:
|
||||||
_LOGGER.error(errors)
|
_LOGGER.error(errors)
|
||||||
hass.components.persistent_notification.async_create(
|
hass.components.persistent_notification.async_create(
|
||||||
"Config error. See dev-info panel for details.",
|
"Config error. See dev-info panel for details.",
|
||||||
"Config validating", "{0}.check_config".format(ha.DOMAIN))
|
"Config validating",
|
||||||
|
"{0}.check_config".format(ha.DOMAIN),
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
if call.service == SERVICE_HOMEASSISTANT_RESTART:
|
if call.service == SERVICE_HOMEASSISTANT_RESTART:
|
||||||
hass.async_create_task(hass.async_stop(RESTART_EXIT_CODE))
|
hass.async_create_task(hass.async_stop(RESTART_EXIT_CODE))
|
||||||
|
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
ha.DOMAIN, SERVICE_HOMEASSISTANT_STOP, async_handle_core_service)
|
ha.DOMAIN, SERVICE_HOMEASSISTANT_STOP, async_handle_core_service
|
||||||
|
)
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
ha.DOMAIN, SERVICE_HOMEASSISTANT_RESTART, async_handle_core_service)
|
ha.DOMAIN, SERVICE_HOMEASSISTANT_RESTART, async_handle_core_service
|
||||||
|
)
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
ha.DOMAIN, SERVICE_CHECK_CONFIG, async_handle_core_service)
|
ha.DOMAIN, SERVICE_CHECK_CONFIG, async_handle_core_service
|
||||||
|
)
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_handle_reload_config(call):
|
def async_handle_reload_config(call):
|
||||||
|
|
@ -203,9 +220,11 @@ def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]:
|
||||||
return
|
return
|
||||||
|
|
||||||
yield from conf_util.async_process_ha_core_config(
|
yield from conf_util.async_process_ha_core_config(
|
||||||
hass, conf.get(ha.DOMAIN) or {})
|
hass, conf.get(ha.DOMAIN) or {}
|
||||||
|
)
|
||||||
|
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG, async_handle_reload_config)
|
ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG, async_handle_reload_config
|
||||||
|
)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
|
||||||
|
|
@ -12,89 +12,109 @@ from requests.exceptions import HTTPError, ConnectTimeout
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ATTRIBUTION, ATTR_DATE, ATTR_TIME, ATTR_ENTITY_ID, CONF_USERNAME,
|
ATTR_ATTRIBUTION,
|
||||||
CONF_PASSWORD, CONF_EXCLUDE, CONF_NAME, CONF_LIGHTS,
|
ATTR_DATE,
|
||||||
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START)
|
ATTR_TIME,
|
||||||
|
ATTR_ENTITY_ID,
|
||||||
|
CONF_USERNAME,
|
||||||
|
CONF_PASSWORD,
|
||||||
|
CONF_EXCLUDE,
|
||||||
|
CONF_NAME,
|
||||||
|
CONF_LIGHTS,
|
||||||
|
EVENT_HOMEASSISTANT_STOP,
|
||||||
|
EVENT_HOMEASSISTANT_START,
|
||||||
|
)
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
from homeassistant.helpers import discovery
|
from homeassistant.helpers import discovery
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
|
|
||||||
REQUIREMENTS = ['abodepy==0.13.1']
|
REQUIREMENTS = ["abodepy==0.13.1"]
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
CONF_ATTRIBUTION = "Data provided by goabode.com"
|
CONF_ATTRIBUTION = "Data provided by goabode.com"
|
||||||
CONF_POLLING = 'polling'
|
CONF_POLLING = "polling"
|
||||||
|
|
||||||
DOMAIN = 'abode'
|
DOMAIN = "abode"
|
||||||
DEFAULT_CACHEDB = './abodepy_cache.pickle'
|
DEFAULT_CACHEDB = "./abodepy_cache.pickle"
|
||||||
|
|
||||||
NOTIFICATION_ID = 'abode_notification'
|
NOTIFICATION_ID = "abode_notification"
|
||||||
NOTIFICATION_TITLE = 'Abode Security Setup'
|
NOTIFICATION_TITLE = "Abode Security Setup"
|
||||||
|
|
||||||
EVENT_ABODE_ALARM = 'abode_alarm'
|
EVENT_ABODE_ALARM = "abode_alarm"
|
||||||
EVENT_ABODE_ALARM_END = 'abode_alarm_end'
|
EVENT_ABODE_ALARM_END = "abode_alarm_end"
|
||||||
EVENT_ABODE_AUTOMATION = 'abode_automation'
|
EVENT_ABODE_AUTOMATION = "abode_automation"
|
||||||
EVENT_ABODE_FAULT = 'abode_panel_fault'
|
EVENT_ABODE_FAULT = "abode_panel_fault"
|
||||||
EVENT_ABODE_RESTORE = 'abode_panel_restore'
|
EVENT_ABODE_RESTORE = "abode_panel_restore"
|
||||||
|
|
||||||
SERVICE_SETTINGS = 'change_setting'
|
SERVICE_SETTINGS = "change_setting"
|
||||||
SERVICE_CAPTURE_IMAGE = 'capture_image'
|
SERVICE_CAPTURE_IMAGE = "capture_image"
|
||||||
SERVICE_TRIGGER = 'trigger_quick_action'
|
SERVICE_TRIGGER = "trigger_quick_action"
|
||||||
|
|
||||||
ATTR_DEVICE_ID = 'device_id'
|
ATTR_DEVICE_ID = "device_id"
|
||||||
ATTR_DEVICE_NAME = 'device_name'
|
ATTR_DEVICE_NAME = "device_name"
|
||||||
ATTR_DEVICE_TYPE = 'device_type'
|
ATTR_DEVICE_TYPE = "device_type"
|
||||||
ATTR_EVENT_CODE = 'event_code'
|
ATTR_EVENT_CODE = "event_code"
|
||||||
ATTR_EVENT_NAME = 'event_name'
|
ATTR_EVENT_NAME = "event_name"
|
||||||
ATTR_EVENT_TYPE = 'event_type'
|
ATTR_EVENT_TYPE = "event_type"
|
||||||
ATTR_EVENT_UTC = 'event_utc'
|
ATTR_EVENT_UTC = "event_utc"
|
||||||
ATTR_SETTING = 'setting'
|
ATTR_SETTING = "setting"
|
||||||
ATTR_USER_NAME = 'user_name'
|
ATTR_USER_NAME = "user_name"
|
||||||
ATTR_VALUE = 'value'
|
ATTR_VALUE = "value"
|
||||||
|
|
||||||
ABODE_DEVICE_ID_LIST_SCHEMA = vol.Schema([str])
|
ABODE_DEVICE_ID_LIST_SCHEMA = vol.Schema([str])
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema({
|
CONFIG_SCHEMA = vol.Schema(
|
||||||
DOMAIN: vol.Schema({
|
{
|
||||||
vol.Required(CONF_USERNAME): cv.string,
|
DOMAIN: vol.Schema(
|
||||||
vol.Required(CONF_PASSWORD): cv.string,
|
{
|
||||||
vol.Optional(CONF_NAME): cv.string,
|
vol.Required(CONF_USERNAME): cv.string,
|
||||||
vol.Optional(CONF_POLLING, default=False): cv.boolean,
|
vol.Required(CONF_PASSWORD): cv.string,
|
||||||
vol.Optional(CONF_EXCLUDE, default=[]): ABODE_DEVICE_ID_LIST_SCHEMA,
|
vol.Optional(CONF_NAME): cv.string,
|
||||||
vol.Optional(CONF_LIGHTS, default=[]): ABODE_DEVICE_ID_LIST_SCHEMA
|
vol.Optional(CONF_POLLING, default=False): cv.boolean,
|
||||||
}),
|
vol.Optional(CONF_EXCLUDE, default=[]): ABODE_DEVICE_ID_LIST_SCHEMA,
|
||||||
}, extra=vol.ALLOW_EXTRA)
|
vol.Optional(CONF_LIGHTS, default=[]): ABODE_DEVICE_ID_LIST_SCHEMA,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
extra=vol.ALLOW_EXTRA,
|
||||||
|
)
|
||||||
|
|
||||||
CHANGE_SETTING_SCHEMA = vol.Schema({
|
CHANGE_SETTING_SCHEMA = vol.Schema(
|
||||||
vol.Required(ATTR_SETTING): cv.string,
|
{vol.Required(ATTR_SETTING): cv.string, vol.Required(ATTR_VALUE): cv.string}
|
||||||
vol.Required(ATTR_VALUE): cv.string
|
)
|
||||||
})
|
|
||||||
|
|
||||||
CAPTURE_IMAGE_SCHEMA = vol.Schema({
|
CAPTURE_IMAGE_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids})
|
||||||
ATTR_ENTITY_ID: cv.entity_ids,
|
|
||||||
})
|
|
||||||
|
|
||||||
TRIGGER_SCHEMA = vol.Schema({
|
TRIGGER_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids})
|
||||||
ATTR_ENTITY_ID: cv.entity_ids,
|
|
||||||
})
|
|
||||||
|
|
||||||
ABODE_PLATFORMS = [
|
ABODE_PLATFORMS = [
|
||||||
'alarm_control_panel', 'binary_sensor', 'lock', 'switch', 'cover',
|
"alarm_control_panel",
|
||||||
'camera', 'light', 'sensor'
|
"binary_sensor",
|
||||||
|
"lock",
|
||||||
|
"switch",
|
||||||
|
"cover",
|
||||||
|
"camera",
|
||||||
|
"light",
|
||||||
|
"sensor",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class AbodeSystem:
|
class AbodeSystem:
|
||||||
"""Abode System class."""
|
"""Abode System class."""
|
||||||
|
|
||||||
def __init__(self, username, password, cache,
|
def __init__(self, username, password, cache, name, polling, exclude, lights):
|
||||||
name, polling, exclude, lights):
|
|
||||||
"""Initialize the system."""
|
"""Initialize the system."""
|
||||||
import abodepy
|
import abodepy
|
||||||
|
|
||||||
self.abode = abodepy.Abode(
|
self.abode = abodepy.Abode(
|
||||||
username, password, auto_login=True, get_devices=True,
|
username,
|
||||||
get_automations=True, cache_path=cache)
|
password,
|
||||||
|
auto_login=True,
|
||||||
|
get_devices=True,
|
||||||
|
get_automations=True,
|
||||||
|
cache_path=cache,
|
||||||
|
)
|
||||||
self.name = name
|
self.name = name
|
||||||
self.polling = polling
|
self.polling = polling
|
||||||
self.exclude = exclude
|
self.exclude = exclude
|
||||||
|
|
@ -113,9 +133,9 @@ class AbodeSystem:
|
||||||
"""Check if a switch device is configured as a light."""
|
"""Check if a switch device is configured as a light."""
|
||||||
import abodepy.helpers.constants as CONST
|
import abodepy.helpers.constants as CONST
|
||||||
|
|
||||||
return (device.generic_type == CONST.TYPE_LIGHT or
|
return device.generic_type == CONST.TYPE_LIGHT or (
|
||||||
(device.generic_type == CONST.TYPE_SWITCH and
|
device.generic_type == CONST.TYPE_SWITCH and device.device_id in self.lights
|
||||||
device.device_id in self.lights))
|
)
|
||||||
|
|
||||||
|
|
||||||
def setup(hass, config):
|
def setup(hass, config):
|
||||||
|
|
@ -133,16 +153,18 @@ def setup(hass, config):
|
||||||
try:
|
try:
|
||||||
cache = hass.config.path(DEFAULT_CACHEDB)
|
cache = hass.config.path(DEFAULT_CACHEDB)
|
||||||
hass.data[DOMAIN] = AbodeSystem(
|
hass.data[DOMAIN] = AbodeSystem(
|
||||||
username, password, cache, name, polling, exclude, lights)
|
username, password, cache, name, polling, exclude, lights
|
||||||
|
)
|
||||||
except (AbodeException, ConnectTimeout, HTTPError) as ex:
|
except (AbodeException, ConnectTimeout, HTTPError) as ex:
|
||||||
_LOGGER.error("Unable to connect to Abode: %s", str(ex))
|
_LOGGER.error("Unable to connect to Abode: %s", str(ex))
|
||||||
|
|
||||||
hass.components.persistent_notification.create(
|
hass.components.persistent_notification.create(
|
||||||
'Error: {}<br />'
|
"Error: {}<br />"
|
||||||
'You will need to restart hass after fixing.'
|
"You will need to restart hass after fixing."
|
||||||
''.format(ex),
|
"".format(ex),
|
||||||
title=NOTIFICATION_TITLE,
|
title=NOTIFICATION_TITLE,
|
||||||
notification_id=NOTIFICATION_ID)
|
notification_id=NOTIFICATION_ID,
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
setup_hass_services(hass)
|
setup_hass_services(hass)
|
||||||
|
|
@ -173,8 +195,11 @@ def setup_hass_services(hass):
|
||||||
"""Capture a new image."""
|
"""Capture a new image."""
|
||||||
entity_ids = call.data.get(ATTR_ENTITY_ID)
|
entity_ids = call.data.get(ATTR_ENTITY_ID)
|
||||||
|
|
||||||
target_devices = [device for device in hass.data[DOMAIN].devices
|
target_devices = [
|
||||||
if device.entity_id in entity_ids]
|
device
|
||||||
|
for device in hass.data[DOMAIN].devices
|
||||||
|
if device.entity_id in entity_ids
|
||||||
|
]
|
||||||
|
|
||||||
for device in target_devices:
|
for device in target_devices:
|
||||||
device.capture()
|
device.capture()
|
||||||
|
|
@ -183,27 +208,31 @@ def setup_hass_services(hass):
|
||||||
"""Trigger a quick action."""
|
"""Trigger a quick action."""
|
||||||
entity_ids = call.data.get(ATTR_ENTITY_ID, None)
|
entity_ids = call.data.get(ATTR_ENTITY_ID, None)
|
||||||
|
|
||||||
target_devices = [device for device in hass.data[DOMAIN].devices
|
target_devices = [
|
||||||
if device.entity_id in entity_ids]
|
device
|
||||||
|
for device in hass.data[DOMAIN].devices
|
||||||
|
if device.entity_id in entity_ids
|
||||||
|
]
|
||||||
|
|
||||||
for device in target_devices:
|
for device in target_devices:
|
||||||
device.trigger()
|
device.trigger()
|
||||||
|
|
||||||
hass.services.register(
|
hass.services.register(
|
||||||
DOMAIN, SERVICE_SETTINGS, change_setting,
|
DOMAIN, SERVICE_SETTINGS, change_setting, schema=CHANGE_SETTING_SCHEMA
|
||||||
schema=CHANGE_SETTING_SCHEMA)
|
)
|
||||||
|
|
||||||
hass.services.register(
|
hass.services.register(
|
||||||
DOMAIN, SERVICE_CAPTURE_IMAGE, capture_image,
|
DOMAIN, SERVICE_CAPTURE_IMAGE, capture_image, schema=CAPTURE_IMAGE_SCHEMA
|
||||||
schema=CAPTURE_IMAGE_SCHEMA)
|
)
|
||||||
|
|
||||||
hass.services.register(
|
hass.services.register(
|
||||||
DOMAIN, SERVICE_TRIGGER, trigger_quick_action,
|
DOMAIN, SERVICE_TRIGGER, trigger_quick_action, schema=TRIGGER_SCHEMA
|
||||||
schema=TRIGGER_SCHEMA)
|
)
|
||||||
|
|
||||||
|
|
||||||
def setup_hass_events(hass):
|
def setup_hass_events(hass):
|
||||||
"""Home Assistant start and stop callbacks."""
|
"""Home Assistant start and stop callbacks."""
|
||||||
|
|
||||||
def startup(event):
|
def startup(event):
|
||||||
"""Listen for push events."""
|
"""Listen for push events."""
|
||||||
hass.data[DOMAIN].abode.events.start()
|
hass.data[DOMAIN].abode.events.start()
|
||||||
|
|
@ -229,28 +258,32 @@ def setup_abode_events(hass):
|
||||||
def event_callback(event, event_json):
|
def event_callback(event, event_json):
|
||||||
"""Handle an event callback from Abode."""
|
"""Handle an event callback from Abode."""
|
||||||
data = {
|
data = {
|
||||||
ATTR_DEVICE_ID: event_json.get(ATTR_DEVICE_ID, ''),
|
ATTR_DEVICE_ID: event_json.get(ATTR_DEVICE_ID, ""),
|
||||||
ATTR_DEVICE_NAME: event_json.get(ATTR_DEVICE_NAME, ''),
|
ATTR_DEVICE_NAME: event_json.get(ATTR_DEVICE_NAME, ""),
|
||||||
ATTR_DEVICE_TYPE: event_json.get(ATTR_DEVICE_TYPE, ''),
|
ATTR_DEVICE_TYPE: event_json.get(ATTR_DEVICE_TYPE, ""),
|
||||||
ATTR_EVENT_CODE: event_json.get(ATTR_EVENT_CODE, ''),
|
ATTR_EVENT_CODE: event_json.get(ATTR_EVENT_CODE, ""),
|
||||||
ATTR_EVENT_NAME: event_json.get(ATTR_EVENT_NAME, ''),
|
ATTR_EVENT_NAME: event_json.get(ATTR_EVENT_NAME, ""),
|
||||||
ATTR_EVENT_TYPE: event_json.get(ATTR_EVENT_TYPE, ''),
|
ATTR_EVENT_TYPE: event_json.get(ATTR_EVENT_TYPE, ""),
|
||||||
ATTR_EVENT_UTC: event_json.get(ATTR_EVENT_UTC, ''),
|
ATTR_EVENT_UTC: event_json.get(ATTR_EVENT_UTC, ""),
|
||||||
ATTR_USER_NAME: event_json.get(ATTR_USER_NAME, ''),
|
ATTR_USER_NAME: event_json.get(ATTR_USER_NAME, ""),
|
||||||
ATTR_DATE: event_json.get(ATTR_DATE, ''),
|
ATTR_DATE: event_json.get(ATTR_DATE, ""),
|
||||||
ATTR_TIME: event_json.get(ATTR_TIME, ''),
|
ATTR_TIME: event_json.get(ATTR_TIME, ""),
|
||||||
}
|
}
|
||||||
|
|
||||||
hass.bus.fire(event, data)
|
hass.bus.fire(event, data)
|
||||||
|
|
||||||
events = [TIMELINE.ALARM_GROUP, TIMELINE.ALARM_END_GROUP,
|
events = [
|
||||||
TIMELINE.PANEL_FAULT_GROUP, TIMELINE.PANEL_RESTORE_GROUP,
|
TIMELINE.ALARM_GROUP,
|
||||||
TIMELINE.AUTOMATION_GROUP]
|
TIMELINE.ALARM_END_GROUP,
|
||||||
|
TIMELINE.PANEL_FAULT_GROUP,
|
||||||
|
TIMELINE.PANEL_RESTORE_GROUP,
|
||||||
|
TIMELINE.AUTOMATION_GROUP,
|
||||||
|
]
|
||||||
|
|
||||||
for event in events:
|
for event in events:
|
||||||
hass.data[DOMAIN].abode.events.add_event_callback(
|
hass.data[DOMAIN].abode.events.add_event_callback(
|
||||||
event,
|
event, partial(event_callback, event)
|
||||||
partial(event_callback, event))
|
)
|
||||||
|
|
||||||
|
|
||||||
class AbodeDevice(Entity):
|
class AbodeDevice(Entity):
|
||||||
|
|
@ -266,7 +299,8 @@ class AbodeDevice(Entity):
|
||||||
"""Subscribe Abode events."""
|
"""Subscribe Abode events."""
|
||||||
self.hass.async_add_job(
|
self.hass.async_add_job(
|
||||||
self._data.abode.events.add_device_callback,
|
self._data.abode.events.add_device_callback,
|
||||||
self._device.device_id, self._update_callback
|
self._device.device_id,
|
||||||
|
self._update_callback,
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
@ -288,10 +322,10 @@ class AbodeDevice(Entity):
|
||||||
"""Return the state attributes."""
|
"""Return the state attributes."""
|
||||||
return {
|
return {
|
||||||
ATTR_ATTRIBUTION: CONF_ATTRIBUTION,
|
ATTR_ATTRIBUTION: CONF_ATTRIBUTION,
|
||||||
'device_id': self._device.device_id,
|
"device_id": self._device.device_id,
|
||||||
'battery_low': self._device.battery_low,
|
"battery_low": self._device.battery_low,
|
||||||
'no_response': self._device.no_response,
|
"no_response": self._device.no_response,
|
||||||
'device_type': self._device.type
|
"device_type": self._device.type,
|
||||||
}
|
}
|
||||||
|
|
||||||
def _update_callback(self, device):
|
def _update_callback(self, device):
|
||||||
|
|
@ -314,7 +348,8 @@ class AbodeAutomation(Entity):
|
||||||
if self._event:
|
if self._event:
|
||||||
self.hass.async_add_job(
|
self.hass.async_add_job(
|
||||||
self._data.abode.events.add_event_callback,
|
self._data.abode.events.add_event_callback,
|
||||||
self._event, self._update_callback
|
self._event,
|
||||||
|
self._update_callback,
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
@ -336,9 +371,9 @@ class AbodeAutomation(Entity):
|
||||||
"""Return the state attributes."""
|
"""Return the state attributes."""
|
||||||
return {
|
return {
|
||||||
ATTR_ATTRIBUTION: CONF_ATTRIBUTION,
|
ATTR_ATTRIBUTION: CONF_ATTRIBUTION,
|
||||||
'automation_id': self._automation.automation_id,
|
"automation_id": self._automation.automation_id,
|
||||||
'type': self._automation.type,
|
"type": self._automation.type,
|
||||||
'sub_type': self._automation.sub_type
|
"sub_type": self._automation.sub_type,
|
||||||
}
|
}
|
||||||
|
|
||||||
def _update_callback(self, device):
|
def _update_callback(self, device):
|
||||||
|
|
|
||||||
|
|
@ -10,51 +10,62 @@ import logging
|
||||||
import ctypes
|
import ctypes
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
from homeassistant.const import CONF_DEVICE, CONF_PORT, CONF_IP_ADDRESS, \
|
from homeassistant.const import (
|
||||||
EVENT_HOMEASSISTANT_STOP
|
CONF_DEVICE,
|
||||||
|
CONF_PORT,
|
||||||
|
CONF_IP_ADDRESS,
|
||||||
|
EVENT_HOMEASSISTANT_STOP,
|
||||||
|
)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
REQUIREMENTS = ['pyads==2.2.6']
|
REQUIREMENTS = ["pyads==2.2.6"]
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DATA_ADS = 'data_ads'
|
DATA_ADS = "data_ads"
|
||||||
|
|
||||||
# Supported Types
|
# Supported Types
|
||||||
ADSTYPE_INT = 'int'
|
ADSTYPE_INT = "int"
|
||||||
ADSTYPE_UINT = 'uint'
|
ADSTYPE_UINT = "uint"
|
||||||
ADSTYPE_BYTE = 'byte'
|
ADSTYPE_BYTE = "byte"
|
||||||
ADSTYPE_BOOL = 'bool'
|
ADSTYPE_BOOL = "bool"
|
||||||
|
|
||||||
DOMAIN = 'ads'
|
DOMAIN = "ads"
|
||||||
|
|
||||||
CONF_ADS_VAR = 'adsvar'
|
CONF_ADS_VAR = "adsvar"
|
||||||
CONF_ADS_VAR_BRIGHTNESS = 'adsvar_brightness'
|
CONF_ADS_VAR_BRIGHTNESS = "adsvar_brightness"
|
||||||
CONF_ADS_TYPE = 'adstype'
|
CONF_ADS_TYPE = "adstype"
|
||||||
CONF_ADS_FACTOR = 'factor'
|
CONF_ADS_FACTOR = "factor"
|
||||||
CONF_ADS_VALUE = 'value'
|
CONF_ADS_VALUE = "value"
|
||||||
|
|
||||||
SERVICE_WRITE_DATA_BY_NAME = 'write_data_by_name'
|
SERVICE_WRITE_DATA_BY_NAME = "write_data_by_name"
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema({
|
CONFIG_SCHEMA = vol.Schema(
|
||||||
DOMAIN: vol.Schema({
|
{
|
||||||
vol.Required(CONF_DEVICE): cv.string,
|
DOMAIN: vol.Schema(
|
||||||
vol.Required(CONF_PORT): cv.port,
|
{
|
||||||
vol.Optional(CONF_IP_ADDRESS): cv.string,
|
vol.Required(CONF_DEVICE): cv.string,
|
||||||
})
|
vol.Required(CONF_PORT): cv.port,
|
||||||
}, extra=vol.ALLOW_EXTRA)
|
vol.Optional(CONF_IP_ADDRESS): cv.string,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
extra=vol.ALLOW_EXTRA,
|
||||||
|
)
|
||||||
|
|
||||||
SCHEMA_SERVICE_WRITE_DATA_BY_NAME = vol.Schema({
|
SCHEMA_SERVICE_WRITE_DATA_BY_NAME = vol.Schema(
|
||||||
vol.Required(CONF_ADS_TYPE):
|
{
|
||||||
vol.In([ADSTYPE_INT, ADSTYPE_UINT, ADSTYPE_BYTE]),
|
vol.Required(CONF_ADS_TYPE): vol.In([ADSTYPE_INT, ADSTYPE_UINT, ADSTYPE_BYTE]),
|
||||||
vol.Required(CONF_ADS_VALUE): cv.match_all,
|
vol.Required(CONF_ADS_VALUE): cv.match_all,
|
||||||
vol.Required(CONF_ADS_VAR): cv.string,
|
vol.Required(CONF_ADS_VAR): cv.string,
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def setup(hass, config):
|
def setup(hass, config):
|
||||||
"""Set up the ADS component."""
|
"""Set up the ADS component."""
|
||||||
import pyads
|
import pyads
|
||||||
|
|
||||||
conf = config[DOMAIN]
|
conf = config[DOMAIN]
|
||||||
|
|
||||||
net_id = conf.get(CONF_DEVICE)
|
net_id = conf.get(CONF_DEVICE)
|
||||||
|
|
@ -79,8 +90,7 @@ def setup(hass, config):
|
||||||
try:
|
try:
|
||||||
ads = AdsHub(client)
|
ads = AdsHub(client)
|
||||||
except pyads.pyads.ADSError:
|
except pyads.pyads.ADSError:
|
||||||
_LOGGER.error(
|
_LOGGER.error("Could not connect to ADS host (netid=%s, port=%s)", net_id, port)
|
||||||
"Could not connect to ADS host (netid=%s, port=%s)", net_id, port)
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
hass.data[DATA_ADS] = ads
|
hass.data[DATA_ADS] = ads
|
||||||
|
|
@ -98,15 +108,18 @@ def setup(hass, config):
|
||||||
_LOGGER.error(err)
|
_LOGGER.error(err)
|
||||||
|
|
||||||
hass.services.register(
|
hass.services.register(
|
||||||
DOMAIN, SERVICE_WRITE_DATA_BY_NAME, handle_write_data_by_name,
|
DOMAIN,
|
||||||
schema=SCHEMA_SERVICE_WRITE_DATA_BY_NAME)
|
SERVICE_WRITE_DATA_BY_NAME,
|
||||||
|
handle_write_data_by_name,
|
||||||
|
schema=SCHEMA_SERVICE_WRITE_DATA_BY_NAME,
|
||||||
|
)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
# Tuple to hold data needed for notification
|
# Tuple to hold data needed for notification
|
||||||
NotificationItem = namedtuple(
|
NotificationItem = namedtuple(
|
||||||
'NotificationItem', 'hnotify huser name plc_datatype callback'
|
"NotificationItem", "hnotify huser name plc_datatype callback"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -128,12 +141,13 @@ class AdsHub:
|
||||||
_LOGGER.debug("Shutting down ADS")
|
_LOGGER.debug("Shutting down ADS")
|
||||||
for notification_item in self._notification_items.values():
|
for notification_item in self._notification_items.values():
|
||||||
self._client.del_device_notification(
|
self._client.del_device_notification(
|
||||||
notification_item.hnotify,
|
notification_item.hnotify, notification_item.huser
|
||||||
notification_item.huser
|
|
||||||
)
|
)
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"Deleting device notification %d, %d",
|
"Deleting device notification %d, %d",
|
||||||
notification_item.hnotify, notification_item.huser)
|
notification_item.hnotify,
|
||||||
|
notification_item.huser,
|
||||||
|
)
|
||||||
self._client.close()
|
self._client.close()
|
||||||
|
|
||||||
def register_device(self, device):
|
def register_device(self, device):
|
||||||
|
|
@ -153,18 +167,20 @@ class AdsHub:
|
||||||
def add_device_notification(self, name, plc_datatype, callback):
|
def add_device_notification(self, name, plc_datatype, callback):
|
||||||
"""Add a notification to the ADS devices."""
|
"""Add a notification to the ADS devices."""
|
||||||
from pyads import NotificationAttrib
|
from pyads import NotificationAttrib
|
||||||
|
|
||||||
attr = NotificationAttrib(ctypes.sizeof(plc_datatype))
|
attr = NotificationAttrib(ctypes.sizeof(plc_datatype))
|
||||||
|
|
||||||
with self._lock:
|
with self._lock:
|
||||||
hnotify, huser = self._client.add_device_notification(
|
hnotify, huser = self._client.add_device_notification(
|
||||||
name, attr, self._device_notification_callback)
|
name, attr, self._device_notification_callback
|
||||||
|
)
|
||||||
hnotify = int(hnotify)
|
hnotify = int(hnotify)
|
||||||
|
|
||||||
_LOGGER.debug(
|
_LOGGER.debug("Added device notification %d for variable %s", hnotify, name)
|
||||||
"Added device notification %d for variable %s", hnotify, name)
|
|
||||||
|
|
||||||
self._notification_items[hnotify] = NotificationItem(
|
self._notification_items[hnotify] = NotificationItem(
|
||||||
hnotify, huser, name, plc_datatype, callback)
|
hnotify, huser, name, plc_datatype, callback
|
||||||
|
)
|
||||||
|
|
||||||
def _device_notification_callback(self, addr, notification, huser):
|
def _device_notification_callback(self, addr, notification, huser):
|
||||||
"""Handle device notifications."""
|
"""Handle device notifications."""
|
||||||
|
|
@ -182,13 +198,13 @@ class AdsHub:
|
||||||
|
|
||||||
# Parse data to desired datatype
|
# Parse data to desired datatype
|
||||||
if notification_item.plc_datatype == self.PLCTYPE_BOOL:
|
if notification_item.plc_datatype == self.PLCTYPE_BOOL:
|
||||||
value = bool(struct.unpack('<?', bytearray(data)[:1])[0])
|
value = bool(struct.unpack("<?", bytearray(data)[:1])[0])
|
||||||
elif notification_item.plc_datatype == self.PLCTYPE_INT:
|
elif notification_item.plc_datatype == self.PLCTYPE_INT:
|
||||||
value = struct.unpack('<h', bytearray(data)[:2])[0]
|
value = struct.unpack("<h", bytearray(data)[:2])[0]
|
||||||
elif notification_item.plc_datatype == self.PLCTYPE_BYTE:
|
elif notification_item.plc_datatype == self.PLCTYPE_BYTE:
|
||||||
value = struct.unpack('<B', bytearray(data)[:1])[0]
|
value = struct.unpack("<B", bytearray(data)[:1])[0]
|
||||||
elif notification_item.plc_datatype == self.PLCTYPE_UINT:
|
elif notification_item.plc_datatype == self.PLCTYPE_UINT:
|
||||||
value = struct.unpack('<H', bytearray(data)[:2])[0]
|
value = struct.unpack("<H", bytearray(data)[:2])[0]
|
||||||
else:
|
else:
|
||||||
value = bytearray(data)
|
value = bytearray(data)
|
||||||
_LOGGER.warning("No callback available for this datatype")
|
_LOGGER.warning("No callback available for this datatype")
|
||||||
|
|
|
||||||
|
|
@ -11,25 +11,31 @@ import logging
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, SERVICE_ALARM_TRIGGER,
|
ATTR_CODE,
|
||||||
SERVICE_ALARM_DISARM, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY,
|
ATTR_CODE_FORMAT,
|
||||||
SERVICE_ALARM_ARM_NIGHT, SERVICE_ALARM_ARM_CUSTOM_BYPASS)
|
ATTR_ENTITY_ID,
|
||||||
|
SERVICE_ALARM_TRIGGER,
|
||||||
|
SERVICE_ALARM_DISARM,
|
||||||
|
SERVICE_ALARM_ARM_HOME,
|
||||||
|
SERVICE_ALARM_ARM_AWAY,
|
||||||
|
SERVICE_ALARM_ARM_NIGHT,
|
||||||
|
SERVICE_ALARM_ARM_CUSTOM_BYPASS,
|
||||||
|
)
|
||||||
from homeassistant.loader import bind_hass
|
from homeassistant.loader import bind_hass
|
||||||
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
from homeassistant.helpers.entity_component import EntityComponent
|
from homeassistant.helpers.entity_component import EntityComponent
|
||||||
|
|
||||||
DOMAIN = 'alarm_control_panel'
|
DOMAIN = "alarm_control_panel"
|
||||||
SCAN_INTERVAL = timedelta(seconds=30)
|
SCAN_INTERVAL = timedelta(seconds=30)
|
||||||
ATTR_CHANGED_BY = 'changed_by'
|
ATTR_CHANGED_BY = "changed_by"
|
||||||
|
|
||||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
ENTITY_ID_FORMAT = DOMAIN + ".{}"
|
||||||
|
|
||||||
ALARM_SERVICE_SCHEMA = vol.Schema({
|
ALARM_SERVICE_SCHEMA = vol.Schema(
|
||||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
{vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Optional(ATTR_CODE): cv.string}
|
||||||
vol.Optional(ATTR_CODE): cv.string,
|
)
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
@bind_hass
|
@bind_hass
|
||||||
|
|
@ -108,33 +114,30 @@ def alarm_arm_custom_bypass(hass, code=None, entity_id=None):
|
||||||
def async_setup(hass, config):
|
def async_setup(hass, config):
|
||||||
"""Track states and offer events for sensors."""
|
"""Track states and offer events for sensors."""
|
||||||
component = hass.data[DOMAIN] = EntityComponent(
|
component = hass.data[DOMAIN] = EntityComponent(
|
||||||
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
|
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL
|
||||||
|
)
|
||||||
|
|
||||||
yield from component.async_setup(config)
|
yield from component.async_setup(config)
|
||||||
|
|
||||||
component.async_register_entity_service(
|
component.async_register_entity_service(
|
||||||
SERVICE_ALARM_DISARM, ALARM_SERVICE_SCHEMA,
|
SERVICE_ALARM_DISARM, ALARM_SERVICE_SCHEMA, "async_alarm_disarm"
|
||||||
'async_alarm_disarm'
|
|
||||||
)
|
)
|
||||||
component.async_register_entity_service(
|
component.async_register_entity_service(
|
||||||
SERVICE_ALARM_ARM_HOME, ALARM_SERVICE_SCHEMA,
|
SERVICE_ALARM_ARM_HOME, ALARM_SERVICE_SCHEMA, "async_alarm_arm_home"
|
||||||
'async_alarm_arm_home'
|
|
||||||
)
|
)
|
||||||
component.async_register_entity_service(
|
component.async_register_entity_service(
|
||||||
SERVICE_ALARM_ARM_AWAY, ALARM_SERVICE_SCHEMA,
|
SERVICE_ALARM_ARM_AWAY, ALARM_SERVICE_SCHEMA, "async_alarm_arm_away"
|
||||||
'async_alarm_arm_away'
|
|
||||||
)
|
)
|
||||||
component.async_register_entity_service(
|
component.async_register_entity_service(
|
||||||
SERVICE_ALARM_ARM_NIGHT, ALARM_SERVICE_SCHEMA,
|
SERVICE_ALARM_ARM_NIGHT, ALARM_SERVICE_SCHEMA, "async_alarm_arm_night"
|
||||||
'async_alarm_arm_night'
|
|
||||||
)
|
)
|
||||||
component.async_register_entity_service(
|
component.async_register_entity_service(
|
||||||
SERVICE_ALARM_ARM_CUSTOM_BYPASS, ALARM_SERVICE_SCHEMA,
|
SERVICE_ALARM_ARM_CUSTOM_BYPASS,
|
||||||
'async_alarm_arm_custom_bypass'
|
ALARM_SERVICE_SCHEMA,
|
||||||
|
"async_alarm_arm_custom_bypass",
|
||||||
)
|
)
|
||||||
component.async_register_entity_service(
|
component.async_register_entity_service(
|
||||||
SERVICE_ALARM_TRIGGER, ALARM_SERVICE_SCHEMA,
|
SERVICE_ALARM_TRIGGER, ALARM_SERVICE_SCHEMA, "async_alarm_trigger"
|
||||||
'async_alarm_trigger'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
@ -228,14 +231,13 @@ class AlarmControlPanel(Entity):
|
||||||
|
|
||||||
This method must be run in the event loop and returns a coroutine.
|
This method must be run in the event loop and returns a coroutine.
|
||||||
"""
|
"""
|
||||||
return self.hass.async_add_executor_job(
|
return self.hass.async_add_executor_job(self.alarm_arm_custom_bypass, code)
|
||||||
self.alarm_arm_custom_bypass, code)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state_attributes(self):
|
def state_attributes(self):
|
||||||
"""Return the state attributes."""
|
"""Return the state attributes."""
|
||||||
state_attr = {
|
state_attr = {
|
||||||
ATTR_CODE_FORMAT: self.code_format,
|
ATTR_CODE_FORMAT: self.code_format,
|
||||||
ATTR_CHANGED_BY: self.changed_by
|
ATTR_CHANGED_BY: self.changed_by,
|
||||||
}
|
}
|
||||||
return state_attr
|
return state_attr
|
||||||
|
|
|
||||||
|
|
@ -10,14 +10,17 @@ from homeassistant.components.abode import CONF_ATTRIBUTION, AbodeDevice
|
||||||
from homeassistant.components.abode import DOMAIN as ABODE_DOMAIN
|
from homeassistant.components.abode import DOMAIN as ABODE_DOMAIN
|
||||||
from homeassistant.components.alarm_control_panel import AlarmControlPanel
|
from homeassistant.components.alarm_control_panel import AlarmControlPanel
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ATTRIBUTION, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
|
ATTR_ATTRIBUTION,
|
||||||
STATE_ALARM_DISARMED)
|
STATE_ALARM_ARMED_AWAY,
|
||||||
|
STATE_ALARM_ARMED_HOME,
|
||||||
|
STATE_ALARM_DISARMED,
|
||||||
|
)
|
||||||
|
|
||||||
DEPENDENCIES = ['abode']
|
DEPENDENCIES = ["abode"]
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
ICON = 'mdi:security'
|
ICON = "mdi:security"
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
|
|
@ -79,7 +82,7 @@ class AbodeAlarm(AbodeDevice, AlarmControlPanel):
|
||||||
"""Return the state attributes."""
|
"""Return the state attributes."""
|
||||||
return {
|
return {
|
||||||
ATTR_ATTRIBUTION: CONF_ATTRIBUTION,
|
ATTR_ATTRIBUTION: CONF_ATTRIBUTION,
|
||||||
'device_id': self._device.device_id,
|
"device_id": self._device.device_id,
|
||||||
'battery_backup': self._device.battery,
|
"battery_backup": self._device.battery,
|
||||||
'cellular_backup': self._device.is_cellular,
|
"cellular_backup": self._device.is_cellular,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,18 +12,20 @@ import voluptuous as vol
|
||||||
import homeassistant.components.alarm_control_panel as alarm
|
import homeassistant.components.alarm_control_panel as alarm
|
||||||
from homeassistant.components.alarmdecoder import DATA_AD, SIGNAL_PANEL_MESSAGE
|
from homeassistant.components.alarmdecoder import DATA_AD, SIGNAL_PANEL_MESSAGE
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_CODE, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
|
ATTR_CODE,
|
||||||
STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED)
|
STATE_ALARM_ARMED_AWAY,
|
||||||
|
STATE_ALARM_ARMED_HOME,
|
||||||
|
STATE_ALARM_DISARMED,
|
||||||
|
STATE_ALARM_TRIGGERED,
|
||||||
|
)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DEPENDENCIES = ['alarmdecoder']
|
DEPENDENCIES = ["alarmdecoder"]
|
||||||
|
|
||||||
SERVICE_ALARM_TOGGLE_CHIME = 'alarmdecoder_alarm_toggle_chime'
|
SERVICE_ALARM_TOGGLE_CHIME = "alarmdecoder_alarm_toggle_chime"
|
||||||
ALARM_TOGGLE_CHIME_SCHEMA = vol.Schema({
|
ALARM_TOGGLE_CHIME_SCHEMA = vol.Schema({vol.Required(ATTR_CODE): cv.string})
|
||||||
vol.Required(ATTR_CODE): cv.string,
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
|
|
@ -37,8 +39,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
device.alarm_toggle_chime(code)
|
device.alarm_toggle_chime(code)
|
||||||
|
|
||||||
hass.services.register(
|
hass.services.register(
|
||||||
alarm.DOMAIN, SERVICE_ALARM_TOGGLE_CHIME, alarm_toggle_chime_handler,
|
alarm.DOMAIN,
|
||||||
schema=ALARM_TOGGLE_CHIME_SCHEMA)
|
SERVICE_ALARM_TOGGLE_CHIME,
|
||||||
|
alarm_toggle_chime_handler,
|
||||||
|
schema=ALARM_TOGGLE_CHIME_SCHEMA,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel):
|
class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel):
|
||||||
|
|
@ -63,7 +68,8 @@ class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel):
|
||||||
def async_added_to_hass(self):
|
def async_added_to_hass(self):
|
||||||
"""Register callbacks."""
|
"""Register callbacks."""
|
||||||
self.hass.helpers.dispatcher.async_dispatcher_connect(
|
self.hass.helpers.dispatcher.async_dispatcher_connect(
|
||||||
SIGNAL_PANEL_MESSAGE, self._message_callback)
|
SIGNAL_PANEL_MESSAGE, self._message_callback
|
||||||
|
)
|
||||||
|
|
||||||
def _message_callback(self, message):
|
def _message_callback(self, message):
|
||||||
"""Handle received messages."""
|
"""Handle received messages."""
|
||||||
|
|
@ -101,7 +107,7 @@ class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel):
|
||||||
@property
|
@property
|
||||||
def code_format(self):
|
def code_format(self):
|
||||||
"""Return one or more digits/characters."""
|
"""Return one or more digits/characters."""
|
||||||
return 'Number'
|
return "Number"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self):
|
def state(self):
|
||||||
|
|
@ -112,15 +118,15 @@ class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel):
|
||||||
def device_state_attributes(self):
|
def device_state_attributes(self):
|
||||||
"""Return the state attributes."""
|
"""Return the state attributes."""
|
||||||
return {
|
return {
|
||||||
'ac_power': self._ac_power,
|
"ac_power": self._ac_power,
|
||||||
'backlight_on': self._backlight_on,
|
"backlight_on": self._backlight_on,
|
||||||
'battery_low': self._battery_low,
|
"battery_low": self._battery_low,
|
||||||
'check_zone': self._check_zone,
|
"check_zone": self._check_zone,
|
||||||
'chime': self._chime,
|
"chime": self._chime,
|
||||||
'entry_delay_off': self._entry_delay_off,
|
"entry_delay_off": self._entry_delay_off,
|
||||||
'programming_mode': self._programming_mode,
|
"programming_mode": self._programming_mode,
|
||||||
'ready': self._ready,
|
"ready": self._ready,
|
||||||
'zone_bypassed': self._zone_bypassed,
|
"zone_bypassed": self._zone_bypassed,
|
||||||
}
|
}
|
||||||
|
|
||||||
def alarm_disarm(self, code=None):
|
def alarm_disarm(self, code=None):
|
||||||
|
|
|
||||||
|
|
@ -13,28 +13,36 @@ import voluptuous as vol
|
||||||
import homeassistant.components.alarm_control_panel as alarm
|
import homeassistant.components.alarm_control_panel as alarm
|
||||||
from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
|
from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_CODE, CONF_NAME, CONF_PASSWORD, CONF_USERNAME, STATE_ALARM_ARMED_AWAY,
|
CONF_CODE,
|
||||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_UNKNOWN)
|
CONF_NAME,
|
||||||
|
CONF_PASSWORD,
|
||||||
|
CONF_USERNAME,
|
||||||
|
STATE_ALARM_ARMED_AWAY,
|
||||||
|
STATE_ALARM_ARMED_HOME,
|
||||||
|
STATE_ALARM_DISARMED,
|
||||||
|
STATE_UNKNOWN,
|
||||||
|
)
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
REQUIREMENTS = ['pyalarmdotcom==0.3.2']
|
REQUIREMENTS = ["pyalarmdotcom==0.3.2"]
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DEFAULT_NAME = 'Alarm.com'
|
DEFAULT_NAME = "Alarm.com"
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||||
vol.Required(CONF_PASSWORD): cv.string,
|
{
|
||||||
vol.Required(CONF_USERNAME): cv.string,
|
vol.Required(CONF_PASSWORD): cv.string,
|
||||||
vol.Optional(CONF_CODE): cv.positive_int,
|
vol.Required(CONF_USERNAME): cv.string,
|
||||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
vol.Optional(CONF_CODE): cv.positive_int,
|
||||||
})
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_setup_platform(hass, config, async_add_entities,
|
def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||||
discovery_info=None):
|
|
||||||
"""Set up a Alarm.com control panel."""
|
"""Set up a Alarm.com control panel."""
|
||||||
name = config.get(CONF_NAME)
|
name = config.get(CONF_NAME)
|
||||||
code = config.get(CONF_CODE)
|
code = config.get(CONF_CODE)
|
||||||
|
|
@ -52,7 +60,8 @@ class AlarmDotCom(alarm.AlarmControlPanel):
|
||||||
def __init__(self, hass, name, code, username, password):
|
def __init__(self, hass, name, code, username, password):
|
||||||
"""Initialize the Alarm.com status."""
|
"""Initialize the Alarm.com status."""
|
||||||
from pyalarmdotcom import Alarmdotcom
|
from pyalarmdotcom import Alarmdotcom
|
||||||
_LOGGER.debug('Setting up Alarm.com...')
|
|
||||||
|
_LOGGER.debug("Setting up Alarm.com...")
|
||||||
self._hass = hass
|
self._hass = hass
|
||||||
self._name = name
|
self._name = name
|
||||||
self._code = str(code) if code else None
|
self._code = str(code) if code else None
|
||||||
|
|
@ -60,8 +69,7 @@ class AlarmDotCom(alarm.AlarmControlPanel):
|
||||||
self._password = password
|
self._password = password
|
||||||
self._websession = async_get_clientsession(self._hass)
|
self._websession = async_get_clientsession(self._hass)
|
||||||
self._state = STATE_UNKNOWN
|
self._state = STATE_UNKNOWN
|
||||||
self._alarm = Alarmdotcom(
|
self._alarm = Alarmdotcom(username, password, self._websession, hass.loop)
|
||||||
username, password, self._websession, hass.loop)
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_login(self):
|
def async_login(self):
|
||||||
|
|
@ -84,27 +92,25 @@ class AlarmDotCom(alarm.AlarmControlPanel):
|
||||||
"""Return one or more digits/characters."""
|
"""Return one or more digits/characters."""
|
||||||
if self._code is None:
|
if self._code is None:
|
||||||
return None
|
return None
|
||||||
if isinstance(self._code, str) and re.search('^\\d+$', self._code):
|
if isinstance(self._code, str) and re.search("^\\d+$", self._code):
|
||||||
return 'Number'
|
return "Number"
|
||||||
return 'Any'
|
return "Any"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self):
|
def state(self):
|
||||||
"""Return the state of the device."""
|
"""Return the state of the device."""
|
||||||
if self._alarm.state.lower() == 'disarmed':
|
if self._alarm.state.lower() == "disarmed":
|
||||||
return STATE_ALARM_DISARMED
|
return STATE_ALARM_DISARMED
|
||||||
if self._alarm.state.lower() == 'armed stay':
|
if self._alarm.state.lower() == "armed stay":
|
||||||
return STATE_ALARM_ARMED_HOME
|
return STATE_ALARM_ARMED_HOME
|
||||||
if self._alarm.state.lower() == 'armed away':
|
if self._alarm.state.lower() == "armed away":
|
||||||
return STATE_ALARM_ARMED_AWAY
|
return STATE_ALARM_ARMED_AWAY
|
||||||
return STATE_UNKNOWN
|
return STATE_UNKNOWN
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device_state_attributes(self):
|
def device_state_attributes(self):
|
||||||
"""Return the state attributes."""
|
"""Return the state attributes."""
|
||||||
return {
|
return {"sensor_status": self._alarm.sensor_status}
|
||||||
'sensor_status': self._alarm.sensor_status
|
|
||||||
}
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_alarm_disarm(self, code=None):
|
def async_alarm_disarm(self, code=None):
|
||||||
|
|
|
||||||
|
|
@ -12,30 +12,40 @@ import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from homeassistant.components.alarm_control_panel import (
|
from homeassistant.components.alarm_control_panel import (
|
||||||
AlarmControlPanel, PLATFORM_SCHEMA)
|
AlarmControlPanel,
|
||||||
|
PLATFORM_SCHEMA,
|
||||||
|
)
|
||||||
from homeassistant.components.arlo import (
|
from homeassistant.components.arlo import (
|
||||||
DATA_ARLO, CONF_ATTRIBUTION, SIGNAL_UPDATE_ARLO)
|
DATA_ARLO,
|
||||||
|
CONF_ATTRIBUTION,
|
||||||
|
SIGNAL_UPDATE_ARLO,
|
||||||
|
)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ATTRIBUTION, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
|
ATTR_ATTRIBUTION,
|
||||||
STATE_ALARM_DISARMED)
|
STATE_ALARM_ARMED_AWAY,
|
||||||
|
STATE_ALARM_ARMED_HOME,
|
||||||
|
STATE_ALARM_DISARMED,
|
||||||
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
ARMED = 'armed'
|
ARMED = "armed"
|
||||||
|
|
||||||
CONF_HOME_MODE_NAME = 'home_mode_name'
|
CONF_HOME_MODE_NAME = "home_mode_name"
|
||||||
CONF_AWAY_MODE_NAME = 'away_mode_name'
|
CONF_AWAY_MODE_NAME = "away_mode_name"
|
||||||
|
|
||||||
DEPENDENCIES = ['arlo']
|
DEPENDENCIES = ["arlo"]
|
||||||
|
|
||||||
DISARMED = 'disarmed'
|
DISARMED = "disarmed"
|
||||||
|
|
||||||
ICON = 'mdi:security'
|
ICON = "mdi:security"
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||||
vol.Optional(CONF_HOME_MODE_NAME, default=ARMED): cv.string,
|
{
|
||||||
vol.Optional(CONF_AWAY_MODE_NAME, default=ARMED): cv.string,
|
vol.Optional(CONF_HOME_MODE_NAME, default=ARMED): cv.string,
|
||||||
})
|
vol.Optional(CONF_AWAY_MODE_NAME, default=ARMED): cv.string,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
|
|
@ -49,8 +59,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
away_mode_name = config.get(CONF_AWAY_MODE_NAME)
|
away_mode_name = config.get(CONF_AWAY_MODE_NAME)
|
||||||
base_stations = []
|
base_stations = []
|
||||||
for base_station in arlo.base_stations:
|
for base_station in arlo.base_stations:
|
||||||
base_stations.append(ArloBaseStation(base_station, home_mode_name,
|
base_stations.append(
|
||||||
away_mode_name))
|
ArloBaseStation(base_station, home_mode_name, away_mode_name)
|
||||||
|
)
|
||||||
add_entities(base_stations, True)
|
add_entities(base_stations, True)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -71,8 +82,7 @@ class ArloBaseStation(AlarmControlPanel):
|
||||||
|
|
||||||
async def async_added_to_hass(self):
|
async def async_added_to_hass(self):
|
||||||
"""Register callbacks."""
|
"""Register callbacks."""
|
||||||
async_dispatcher_connect(
|
async_dispatcher_connect(self.hass, SIGNAL_UPDATE_ARLO, self._update_callback)
|
||||||
self.hass, SIGNAL_UPDATE_ARLO, self._update_callback)
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _update_callback(self):
|
def _update_callback(self):
|
||||||
|
|
@ -115,7 +125,7 @@ class ArloBaseStation(AlarmControlPanel):
|
||||||
"""Return the state attributes."""
|
"""Return the state attributes."""
|
||||||
return {
|
return {
|
||||||
ATTR_ATTRIBUTION: CONF_ATTRIBUTION,
|
ATTR_ATTRIBUTION: CONF_ATTRIBUTION,
|
||||||
'device_id': self._base_station.device_id
|
"device_id": self._base_station.device_id,
|
||||||
}
|
}
|
||||||
|
|
||||||
def _get_state_from_mode(self, mode):
|
def _get_state_from_mode(self, mode):
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,14 @@ import logging
|
||||||
|
|
||||||
from homeassistant.components.alarm_control_panel import AlarmControlPanel
|
from homeassistant.components.alarm_control_panel import AlarmControlPanel
|
||||||
from homeassistant.components.canary import DATA_CANARY
|
from homeassistant.components.canary import DATA_CANARY
|
||||||
from homeassistant.const import STATE_ALARM_DISARMED, STATE_ALARM_ARMED_AWAY, \
|
from homeassistant.const import (
|
||||||
STATE_ALARM_ARMED_NIGHT, STATE_ALARM_ARMED_HOME
|
STATE_ALARM_DISARMED,
|
||||||
|
STATE_ALARM_ARMED_AWAY,
|
||||||
|
STATE_ALARM_ARMED_NIGHT,
|
||||||
|
STATE_ALARM_ARMED_HOME,
|
||||||
|
)
|
||||||
|
|
||||||
DEPENDENCIES = ['canary']
|
DEPENDENCIES = ["canary"]
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -44,8 +48,11 @@ class CanaryAlarm(AlarmControlPanel):
|
||||||
@property
|
@property
|
||||||
def state(self):
|
def state(self):
|
||||||
"""Return the state of the device."""
|
"""Return the state of the device."""
|
||||||
from canary.api import LOCATION_MODE_AWAY, LOCATION_MODE_HOME, \
|
from canary.api import (
|
||||||
LOCATION_MODE_NIGHT
|
LOCATION_MODE_AWAY,
|
||||||
|
LOCATION_MODE_HOME,
|
||||||
|
LOCATION_MODE_NIGHT,
|
||||||
|
)
|
||||||
|
|
||||||
location = self._data.get_location(self._location_id)
|
location = self._data.get_location(self._location_id)
|
||||||
|
|
||||||
|
|
@ -65,27 +72,27 @@ class CanaryAlarm(AlarmControlPanel):
|
||||||
def device_state_attributes(self):
|
def device_state_attributes(self):
|
||||||
"""Return the state attributes."""
|
"""Return the state attributes."""
|
||||||
location = self._data.get_location(self._location_id)
|
location = self._data.get_location(self._location_id)
|
||||||
return {
|
return {"private": location.is_private}
|
||||||
'private': location.is_private
|
|
||||||
}
|
|
||||||
|
|
||||||
def alarm_disarm(self, code=None):
|
def alarm_disarm(self, code=None):
|
||||||
"""Send disarm command."""
|
"""Send disarm command."""
|
||||||
location = self._data.get_location(self._location_id)
|
location = self._data.get_location(self._location_id)
|
||||||
self._data.set_location_mode(self._location_id, location.mode.name,
|
self._data.set_location_mode(self._location_id, location.mode.name, True)
|
||||||
True)
|
|
||||||
|
|
||||||
def alarm_arm_home(self, code=None):
|
def alarm_arm_home(self, code=None):
|
||||||
"""Send arm home command."""
|
"""Send arm home command."""
|
||||||
from canary.api import LOCATION_MODE_HOME
|
from canary.api import LOCATION_MODE_HOME
|
||||||
|
|
||||||
self._data.set_location_mode(self._location_id, LOCATION_MODE_HOME)
|
self._data.set_location_mode(self._location_id, LOCATION_MODE_HOME)
|
||||||
|
|
||||||
def alarm_arm_away(self, code=None):
|
def alarm_arm_away(self, code=None):
|
||||||
"""Send arm away command."""
|
"""Send arm away command."""
|
||||||
from canary.api import LOCATION_MODE_AWAY
|
from canary.api import LOCATION_MODE_AWAY
|
||||||
|
|
||||||
self._data.set_location_mode(self._location_id, LOCATION_MODE_AWAY)
|
self._data.set_location_mode(self._location_id, LOCATION_MODE_AWAY)
|
||||||
|
|
||||||
def alarm_arm_night(self, code=None):
|
def alarm_arm_night(self, code=None):
|
||||||
"""Send arm night command."""
|
"""Send arm night command."""
|
||||||
from canary.api import LOCATION_MODE_NIGHT
|
from canary.api import LOCATION_MODE_NIGHT
|
||||||
|
|
||||||
self._data.set_location_mode(self._location_id, LOCATION_MODE_NIGHT)
|
self._data.set_location_mode(self._location_id, LOCATION_MODE_NIGHT)
|
||||||
|
|
|
||||||
|
|
@ -14,25 +14,33 @@ import voluptuous as vol
|
||||||
import homeassistant.components.alarm_control_panel as alarm
|
import homeassistant.components.alarm_control_panel as alarm
|
||||||
from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
|
from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_HOST, CONF_NAME, CONF_PORT, STATE_ALARM_ARMED_AWAY,
|
CONF_HOST,
|
||||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_UNKNOWN)
|
CONF_NAME,
|
||||||
|
CONF_PORT,
|
||||||
|
STATE_ALARM_ARMED_AWAY,
|
||||||
|
STATE_ALARM_ARMED_HOME,
|
||||||
|
STATE_ALARM_DISARMED,
|
||||||
|
STATE_UNKNOWN,
|
||||||
|
)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
REQUIREMENTS = ['concord232==0.15']
|
REQUIREMENTS = ["concord232==0.15"]
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DEFAULT_HOST = 'localhost'
|
DEFAULT_HOST = "localhost"
|
||||||
DEFAULT_NAME = 'CONCORD232'
|
DEFAULT_NAME = "CONCORD232"
|
||||||
DEFAULT_PORT = 5007
|
DEFAULT_PORT = 5007
|
||||||
|
|
||||||
SCAN_INTERVAL = timedelta(seconds=10)
|
SCAN_INTERVAL = timedelta(seconds=10)
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||||
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
|
{
|
||||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
|
||||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||||
})
|
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
|
|
@ -41,7 +49,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
host = config.get(CONF_HOST)
|
host = config.get(CONF_HOST)
|
||||||
port = config.get(CONF_PORT)
|
port = config.get(CONF_PORT)
|
||||||
|
|
||||||
url = 'http://{}:{}'.format(host, port)
|
url = "http://{}:{}".format(host, port)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
add_entities([Concord232Alarm(hass, url, name)])
|
add_entities([Concord232Alarm(hass, url, name)])
|
||||||
|
|
@ -80,7 +88,7 @@ class Concord232Alarm(alarm.AlarmControlPanel):
|
||||||
@property
|
@property
|
||||||
def code_format(self):
|
def code_format(self):
|
||||||
"""Return the characters if code is defined."""
|
"""Return the characters if code is defined."""
|
||||||
return 'Number'
|
return "Number"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self):
|
def state(self):
|
||||||
|
|
@ -92,16 +100,18 @@ class Concord232Alarm(alarm.AlarmControlPanel):
|
||||||
try:
|
try:
|
||||||
part = self._alarm.list_partitions()[0]
|
part = self._alarm.list_partitions()[0]
|
||||||
except requests.exceptions.ConnectionError as ex:
|
except requests.exceptions.ConnectionError as ex:
|
||||||
_LOGGER.error("Unable to connect to %(host)s: %(reason)s",
|
_LOGGER.error(
|
||||||
dict(host=self._url, reason=ex))
|
"Unable to connect to %(host)s: %(reason)s",
|
||||||
|
dict(host=self._url, reason=ex),
|
||||||
|
)
|
||||||
newstate = STATE_UNKNOWN
|
newstate = STATE_UNKNOWN
|
||||||
except IndexError:
|
except IndexError:
|
||||||
_LOGGER.error("Concord232 reports no partitions")
|
_LOGGER.error("Concord232 reports no partitions")
|
||||||
newstate = STATE_UNKNOWN
|
newstate = STATE_UNKNOWN
|
||||||
|
|
||||||
if part['arming_level'] == 'Off':
|
if part["arming_level"] == "Off":
|
||||||
newstate = STATE_ALARM_DISARMED
|
newstate = STATE_ALARM_DISARMED
|
||||||
elif 'Home' in part['arming_level']:
|
elif "Home" in part["arming_level"]:
|
||||||
newstate = STATE_ALARM_ARMED_HOME
|
newstate = STATE_ALARM_ARMED_HOME
|
||||||
else:
|
else:
|
||||||
newstate = STATE_ALARM_ARMED_AWAY
|
newstate = STATE_ALARM_ARMED_AWAY
|
||||||
|
|
@ -117,8 +127,8 @@ class Concord232Alarm(alarm.AlarmControlPanel):
|
||||||
|
|
||||||
def alarm_arm_home(self, code=None):
|
def alarm_arm_home(self, code=None):
|
||||||
"""Send arm home command."""
|
"""Send arm home command."""
|
||||||
self._alarm.arm('stay')
|
self._alarm.arm("stay")
|
||||||
|
|
||||||
def alarm_arm_away(self, code=None):
|
def alarm_arm_away(self, code=None):
|
||||||
"""Send arm away command."""
|
"""Send arm away command."""
|
||||||
self._alarm.arm('away')
|
self._alarm.arm("away")
|
||||||
|
|
|
||||||
|
|
@ -7,42 +7,57 @@ https://home-assistant.io/components/demo/
|
||||||
import datetime
|
import datetime
|
||||||
from homeassistant.components.alarm_control_panel import manual
|
from homeassistant.components.alarm_control_panel import manual
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_CUSTOM_BYPASS,
|
STATE_ALARM_ARMED_AWAY,
|
||||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT,
|
STATE_ALARM_ARMED_CUSTOM_BYPASS,
|
||||||
STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, CONF_DELAY_TIME,
|
STATE_ALARM_ARMED_HOME,
|
||||||
CONF_PENDING_TIME, CONF_TRIGGER_TIME)
|
STATE_ALARM_ARMED_NIGHT,
|
||||||
|
STATE_ALARM_DISARMED,
|
||||||
|
STATE_ALARM_TRIGGERED,
|
||||||
|
CONF_DELAY_TIME,
|
||||||
|
CONF_PENDING_TIME,
|
||||||
|
CONF_TRIGGER_TIME,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
"""Set up the Demo alarm control panel platform."""
|
"""Set up the Demo alarm control panel platform."""
|
||||||
add_entities([
|
add_entities(
|
||||||
manual.ManualAlarm(hass, 'Alarm', '1234', None, False, {
|
[
|
||||||
STATE_ALARM_ARMED_AWAY: {
|
manual.ManualAlarm(
|
||||||
CONF_DELAY_TIME: datetime.timedelta(seconds=0),
|
hass,
|
||||||
CONF_PENDING_TIME: datetime.timedelta(seconds=5),
|
"Alarm",
|
||||||
CONF_TRIGGER_TIME: datetime.timedelta(seconds=10),
|
"1234",
|
||||||
},
|
None,
|
||||||
STATE_ALARM_ARMED_HOME: {
|
False,
|
||||||
CONF_DELAY_TIME: datetime.timedelta(seconds=0),
|
{
|
||||||
CONF_PENDING_TIME: datetime.timedelta(seconds=5),
|
STATE_ALARM_ARMED_AWAY: {
|
||||||
CONF_TRIGGER_TIME: datetime.timedelta(seconds=10),
|
CONF_DELAY_TIME: datetime.timedelta(seconds=0),
|
||||||
},
|
CONF_PENDING_TIME: datetime.timedelta(seconds=5),
|
||||||
STATE_ALARM_ARMED_NIGHT: {
|
CONF_TRIGGER_TIME: datetime.timedelta(seconds=10),
|
||||||
CONF_DELAY_TIME: datetime.timedelta(seconds=0),
|
},
|
||||||
CONF_PENDING_TIME: datetime.timedelta(seconds=5),
|
STATE_ALARM_ARMED_HOME: {
|
||||||
CONF_TRIGGER_TIME: datetime.timedelta(seconds=10),
|
CONF_DELAY_TIME: datetime.timedelta(seconds=0),
|
||||||
},
|
CONF_PENDING_TIME: datetime.timedelta(seconds=5),
|
||||||
STATE_ALARM_DISARMED: {
|
CONF_TRIGGER_TIME: datetime.timedelta(seconds=10),
|
||||||
CONF_DELAY_TIME: datetime.timedelta(seconds=0),
|
},
|
||||||
CONF_TRIGGER_TIME: datetime.timedelta(seconds=10),
|
STATE_ALARM_ARMED_NIGHT: {
|
||||||
},
|
CONF_DELAY_TIME: datetime.timedelta(seconds=0),
|
||||||
STATE_ALARM_ARMED_CUSTOM_BYPASS: {
|
CONF_PENDING_TIME: datetime.timedelta(seconds=5),
|
||||||
CONF_DELAY_TIME: datetime.timedelta(seconds=0),
|
CONF_TRIGGER_TIME: datetime.timedelta(seconds=10),
|
||||||
CONF_PENDING_TIME: datetime.timedelta(seconds=5),
|
},
|
||||||
CONF_TRIGGER_TIME: datetime.timedelta(seconds=10),
|
STATE_ALARM_DISARMED: {
|
||||||
},
|
CONF_DELAY_TIME: datetime.timedelta(seconds=0),
|
||||||
STATE_ALARM_TRIGGERED: {
|
CONF_TRIGGER_TIME: datetime.timedelta(seconds=10),
|
||||||
CONF_PENDING_TIME: datetime.timedelta(seconds=5),
|
},
|
||||||
},
|
STATE_ALARM_ARMED_CUSTOM_BYPASS: {
|
||||||
}),
|
CONF_DELAY_TIME: datetime.timedelta(seconds=0),
|
||||||
])
|
CONF_PENDING_TIME: datetime.timedelta(seconds=5),
|
||||||
|
CONF_TRIGGER_TIME: datetime.timedelta(seconds=10),
|
||||||
|
},
|
||||||
|
STATE_ALARM_TRIGGERED: {
|
||||||
|
CONF_PENDING_TIME: datetime.timedelta(seconds=5)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -11,26 +11,33 @@ import requests
|
||||||
|
|
||||||
import homeassistant.components.alarm_control_panel as alarm
|
import homeassistant.components.alarm_control_panel as alarm
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME,
|
STATE_ALARM_DISARMED,
|
||||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_TRIGGERED,
|
STATE_ALARM_ARMED_HOME,
|
||||||
STATE_ALARM_ARMED_NIGHT)
|
STATE_ALARM_ARMED_AWAY,
|
||||||
|
STATE_ALARM_TRIGGERED,
|
||||||
|
STATE_ALARM_ARMED_NIGHT,
|
||||||
|
)
|
||||||
from homeassistant.components.egardia import (
|
from homeassistant.components.egardia import (
|
||||||
EGARDIA_DEVICE, EGARDIA_SERVER,
|
EGARDIA_DEVICE,
|
||||||
REPORT_SERVER_CODES_IGNORE, CONF_REPORT_SERVER_CODES,
|
EGARDIA_SERVER,
|
||||||
CONF_REPORT_SERVER_ENABLED, CONF_REPORT_SERVER_PORT
|
REPORT_SERVER_CODES_IGNORE,
|
||||||
)
|
CONF_REPORT_SERVER_CODES,
|
||||||
DEPENDENCIES = ['egardia']
|
CONF_REPORT_SERVER_ENABLED,
|
||||||
|
CONF_REPORT_SERVER_PORT,
|
||||||
|
)
|
||||||
|
|
||||||
|
DEPENDENCIES = ["egardia"]
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
STATES = {
|
STATES = {
|
||||||
'ARM': STATE_ALARM_ARMED_AWAY,
|
"ARM": STATE_ALARM_ARMED_AWAY,
|
||||||
'DAY HOME': STATE_ALARM_ARMED_HOME,
|
"DAY HOME": STATE_ALARM_ARMED_HOME,
|
||||||
'DISARM': STATE_ALARM_DISARMED,
|
"DISARM": STATE_ALARM_DISARMED,
|
||||||
'ARMHOME': STATE_ALARM_ARMED_HOME,
|
"ARMHOME": STATE_ALARM_ARMED_HOME,
|
||||||
'HOME': STATE_ALARM_ARMED_HOME,
|
"HOME": STATE_ALARM_ARMED_HOME,
|
||||||
'NIGHT HOME': STATE_ALARM_ARMED_NIGHT,
|
"NIGHT HOME": STATE_ALARM_ARMED_NIGHT,
|
||||||
'TRIGGERED': STATE_ALARM_TRIGGERED
|
"TRIGGERED": STATE_ALARM_TRIGGERED,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -39,11 +46,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
if discovery_info is None:
|
if discovery_info is None:
|
||||||
return
|
return
|
||||||
device = EgardiaAlarm(
|
device = EgardiaAlarm(
|
||||||
discovery_info['name'],
|
discovery_info["name"],
|
||||||
hass.data[EGARDIA_DEVICE],
|
hass.data[EGARDIA_DEVICE],
|
||||||
discovery_info[CONF_REPORT_SERVER_ENABLED],
|
discovery_info[CONF_REPORT_SERVER_ENABLED],
|
||||||
discovery_info.get(CONF_REPORT_SERVER_CODES),
|
discovery_info.get(CONF_REPORT_SERVER_CODES),
|
||||||
discovery_info[CONF_REPORT_SERVER_PORT])
|
discovery_info[CONF_REPORT_SERVER_PORT],
|
||||||
|
)
|
||||||
# add egardia alarm device
|
# add egardia alarm device
|
||||||
add_entities([device], True)
|
add_entities([device], True)
|
||||||
|
|
||||||
|
|
@ -51,8 +59,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
class EgardiaAlarm(alarm.AlarmControlPanel):
|
class EgardiaAlarm(alarm.AlarmControlPanel):
|
||||||
"""Representation of a Egardia alarm."""
|
"""Representation of a Egardia alarm."""
|
||||||
|
|
||||||
def __init__(self, name, egardiasystem,
|
def __init__(
|
||||||
rs_enabled=False, rs_codes=None, rs_port=52010):
|
self, name, egardiasystem, rs_enabled=False, rs_codes=None, rs_port=52010
|
||||||
|
):
|
||||||
"""Initialize the Egardia alarm."""
|
"""Initialize the Egardia alarm."""
|
||||||
self._name = name
|
self._name = name
|
||||||
self._egardiasystem = egardiasystem
|
self._egardiasystem = egardiasystem
|
||||||
|
|
@ -66,8 +75,7 @@ class EgardiaAlarm(alarm.AlarmControlPanel):
|
||||||
"""Add Egardiaserver callback if enabled."""
|
"""Add Egardiaserver callback if enabled."""
|
||||||
if self._rs_enabled:
|
if self._rs_enabled:
|
||||||
_LOGGER.debug("Registering callback to Egardiaserver")
|
_LOGGER.debug("Registering callback to Egardiaserver")
|
||||||
self.hass.data[EGARDIA_SERVER].register_callback(
|
self.hass.data[EGARDIA_SERVER].register_callback(self.handle_status_event)
|
||||||
self.handle_status_event)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
|
|
@ -88,7 +96,7 @@ class EgardiaAlarm(alarm.AlarmControlPanel):
|
||||||
|
|
||||||
def handle_status_event(self, event):
|
def handle_status_event(self, event):
|
||||||
"""Handle the Egardia system status event."""
|
"""Handle the Egardia system status event."""
|
||||||
statuscode = event.get('status')
|
statuscode = event.get("status")
|
||||||
if statuscode is not None:
|
if statuscode is not None:
|
||||||
status = self.lookupstatusfromcode(statuscode)
|
status = self.lookupstatusfromcode(statuscode)
|
||||||
self.parsestatus(status)
|
self.parsestatus(status)
|
||||||
|
|
@ -96,10 +104,15 @@ class EgardiaAlarm(alarm.AlarmControlPanel):
|
||||||
|
|
||||||
def lookupstatusfromcode(self, statuscode):
|
def lookupstatusfromcode(self, statuscode):
|
||||||
"""Look at the rs_codes and returns the status from the code."""
|
"""Look at the rs_codes and returns the status from the code."""
|
||||||
status = next((
|
status = next(
|
||||||
status_group.upper() for status_group, codes
|
(
|
||||||
in self._rs_codes.items() for code in codes
|
status_group.upper()
|
||||||
if statuscode == code), 'UNKNOWN')
|
for status_group, codes in self._rs_codes.items()
|
||||||
|
for code in codes
|
||||||
|
if statuscode == code
|
||||||
|
),
|
||||||
|
"UNKNOWN",
|
||||||
|
)
|
||||||
return status
|
return status
|
||||||
|
|
||||||
def parsestatus(self, status):
|
def parsestatus(self, status):
|
||||||
|
|
@ -124,21 +137,29 @@ class EgardiaAlarm(alarm.AlarmControlPanel):
|
||||||
try:
|
try:
|
||||||
self._egardiasystem.alarm_disarm()
|
self._egardiasystem.alarm_disarm()
|
||||||
except requests.exceptions.RequestException as err:
|
except requests.exceptions.RequestException as err:
|
||||||
_LOGGER.error("Egardia device exception occurred when "
|
_LOGGER.error(
|
||||||
"sending disarm command: %s", err)
|
"Egardia device exception occurred when " "sending disarm command: %s",
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
|
||||||
def alarm_arm_home(self, code=None):
|
def alarm_arm_home(self, code=None):
|
||||||
"""Send arm home command."""
|
"""Send arm home command."""
|
||||||
try:
|
try:
|
||||||
self._egardiasystem.alarm_arm_home()
|
self._egardiasystem.alarm_arm_home()
|
||||||
except requests.exceptions.RequestException as err:
|
except requests.exceptions.RequestException as err:
|
||||||
_LOGGER.error("Egardia device exception occurred when "
|
_LOGGER.error(
|
||||||
"sending arm home command: %s", err)
|
"Egardia device exception occurred when "
|
||||||
|
"sending arm home command: %s",
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
|
||||||
def alarm_arm_away(self, code=None):
|
def alarm_arm_away(self, code=None):
|
||||||
"""Send arm away command."""
|
"""Send arm away command."""
|
||||||
try:
|
try:
|
||||||
self._egardiasystem.alarm_arm_away()
|
self._egardiasystem.alarm_arm_away()
|
||||||
except requests.exceptions.RequestException as err:
|
except requests.exceptions.RequestException as err:
|
||||||
_LOGGER.error("Egardia device exception occurred when "
|
_LOGGER.error(
|
||||||
"sending arm away command: %s", err)
|
"Egardia device exception occurred when "
|
||||||
|
"sending arm away command: %s",
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -14,29 +14,43 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
import homeassistant.components.alarm_control_panel as alarm
|
import homeassistant.components.alarm_control_panel as alarm
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.components.envisalink import (
|
from homeassistant.components.envisalink import (
|
||||||
DATA_EVL, EnvisalinkDevice, PARTITION_SCHEMA, CONF_CODE, CONF_PANIC,
|
DATA_EVL,
|
||||||
CONF_PARTITIONNAME, SIGNAL_KEYPAD_UPDATE, SIGNAL_PARTITION_UPDATE)
|
EnvisalinkDevice,
|
||||||
|
PARTITION_SCHEMA,
|
||||||
|
CONF_CODE,
|
||||||
|
CONF_PANIC,
|
||||||
|
CONF_PARTITIONNAME,
|
||||||
|
SIGNAL_KEYPAD_UPDATE,
|
||||||
|
SIGNAL_PARTITION_UPDATE,
|
||||||
|
)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
|
STATE_ALARM_ARMED_AWAY,
|
||||||
STATE_UNKNOWN, STATE_ALARM_TRIGGERED, STATE_ALARM_PENDING, ATTR_ENTITY_ID)
|
STATE_ALARM_ARMED_HOME,
|
||||||
|
STATE_ALARM_DISARMED,
|
||||||
|
STATE_UNKNOWN,
|
||||||
|
STATE_ALARM_TRIGGERED,
|
||||||
|
STATE_ALARM_PENDING,
|
||||||
|
ATTR_ENTITY_ID,
|
||||||
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DEPENDENCIES = ['envisalink']
|
DEPENDENCIES = ["envisalink"]
|
||||||
|
|
||||||
SERVICE_ALARM_KEYPRESS = 'envisalink_alarm_keypress'
|
SERVICE_ALARM_KEYPRESS = "envisalink_alarm_keypress"
|
||||||
ATTR_KEYPRESS = 'keypress'
|
ATTR_KEYPRESS = "keypress"
|
||||||
ALARM_KEYPRESS_SCHEMA = vol.Schema({
|
ALARM_KEYPRESS_SCHEMA = vol.Schema(
|
||||||
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
|
{
|
||||||
vol.Required(ATTR_KEYPRESS): cv.string
|
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
|
||||||
})
|
vol.Required(ATTR_KEYPRESS): cv.string,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_setup_platform(hass, config, async_add_entities,
|
def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||||
discovery_info=None):
|
|
||||||
"""Perform the setup for Envisalink alarm panels."""
|
"""Perform the setup for Envisalink alarm panels."""
|
||||||
configured_partitions = discovery_info['partitions']
|
configured_partitions = discovery_info["partitions"]
|
||||||
code = discovery_info[CONF_CODE]
|
code = discovery_info[CONF_CODE]
|
||||||
panic_type = discovery_info[CONF_PANIC]
|
panic_type = discovery_info[CONF_PANIC]
|
||||||
|
|
||||||
|
|
@ -49,8 +63,8 @@ def async_setup_platform(hass, config, async_add_entities,
|
||||||
device_config_data[CONF_PARTITIONNAME],
|
device_config_data[CONF_PARTITIONNAME],
|
||||||
code,
|
code,
|
||||||
panic_type,
|
panic_type,
|
||||||
hass.data[DATA_EVL].alarm_state['partition'][part_num],
|
hass.data[DATA_EVL].alarm_state["partition"][part_num],
|
||||||
hass.data[DATA_EVL]
|
hass.data[DATA_EVL],
|
||||||
)
|
)
|
||||||
devices.append(device)
|
devices.append(device)
|
||||||
|
|
||||||
|
|
@ -62,15 +76,19 @@ def async_setup_platform(hass, config, async_add_entities,
|
||||||
entity_ids = service.data.get(ATTR_ENTITY_ID)
|
entity_ids = service.data.get(ATTR_ENTITY_ID)
|
||||||
keypress = service.data.get(ATTR_KEYPRESS)
|
keypress = service.data.get(ATTR_KEYPRESS)
|
||||||
|
|
||||||
target_devices = [device for device in devices
|
target_devices = [
|
||||||
if device.entity_id in entity_ids]
|
device for device in devices if device.entity_id in entity_ids
|
||||||
|
]
|
||||||
|
|
||||||
for device in target_devices:
|
for device in target_devices:
|
||||||
device.async_alarm_keypress(keypress)
|
device.async_alarm_keypress(keypress)
|
||||||
|
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
alarm.DOMAIN, SERVICE_ALARM_KEYPRESS, alarm_keypress_handler,
|
alarm.DOMAIN,
|
||||||
schema=ALARM_KEYPRESS_SCHEMA)
|
SERVICE_ALARM_KEYPRESS,
|
||||||
|
alarm_keypress_handler,
|
||||||
|
schema=ALARM_KEYPRESS_SCHEMA,
|
||||||
|
)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
@ -78,8 +96,9 @@ def async_setup_platform(hass, config, async_add_entities,
|
||||||
class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
|
class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
|
||||||
"""Representation of an Envisalink-based alarm panel."""
|
"""Representation of an Envisalink-based alarm panel."""
|
||||||
|
|
||||||
def __init__(self, hass, partition_number, alarm_name, code, panic_type,
|
def __init__(
|
||||||
info, controller):
|
self, hass, partition_number, alarm_name, code, panic_type, info, controller
|
||||||
|
):
|
||||||
"""Initialize the alarm panel."""
|
"""Initialize the alarm panel."""
|
||||||
self._partition_number = partition_number
|
self._partition_number = partition_number
|
||||||
self._code = code
|
self._code = code
|
||||||
|
|
@ -91,10 +110,10 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_added_to_hass(self):
|
def async_added_to_hass(self):
|
||||||
"""Register callbacks."""
|
"""Register callbacks."""
|
||||||
|
async_dispatcher_connect(self.hass, SIGNAL_KEYPAD_UPDATE, self._update_callback)
|
||||||
async_dispatcher_connect(
|
async_dispatcher_connect(
|
||||||
self.hass, SIGNAL_KEYPAD_UPDATE, self._update_callback)
|
self.hass, SIGNAL_PARTITION_UPDATE, self._update_callback
|
||||||
async_dispatcher_connect(
|
)
|
||||||
self.hass, SIGNAL_PARTITION_UPDATE, self._update_callback)
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _update_callback(self, partition):
|
def _update_callback(self, partition):
|
||||||
|
|
@ -107,24 +126,24 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
|
||||||
"""Regex for code format or None if no code is required."""
|
"""Regex for code format or None if no code is required."""
|
||||||
if self._code:
|
if self._code:
|
||||||
return None
|
return None
|
||||||
return 'Number'
|
return "Number"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self):
|
def state(self):
|
||||||
"""Return the state of the device."""
|
"""Return the state of the device."""
|
||||||
state = STATE_UNKNOWN
|
state = STATE_UNKNOWN
|
||||||
|
|
||||||
if self._info['status']['alarm']:
|
if self._info["status"]["alarm"]:
|
||||||
state = STATE_ALARM_TRIGGERED
|
state = STATE_ALARM_TRIGGERED
|
||||||
elif self._info['status']['armed_away']:
|
elif self._info["status"]["armed_away"]:
|
||||||
state = STATE_ALARM_ARMED_AWAY
|
state = STATE_ALARM_ARMED_AWAY
|
||||||
elif self._info['status']['armed_stay']:
|
elif self._info["status"]["armed_stay"]:
|
||||||
state = STATE_ALARM_ARMED_HOME
|
state = STATE_ALARM_ARMED_HOME
|
||||||
elif self._info['status']['exit_delay']:
|
elif self._info["status"]["exit_delay"]:
|
||||||
state = STATE_ALARM_PENDING
|
state = STATE_ALARM_PENDING
|
||||||
elif self._info['status']['entry_delay']:
|
elif self._info["status"]["entry_delay"]:
|
||||||
state = STATE_ALARM_PENDING
|
state = STATE_ALARM_PENDING
|
||||||
elif self._info['status']['alpha']:
|
elif self._info["status"]["alpha"]:
|
||||||
state = STATE_ALARM_DISARMED
|
state = STATE_ALARM_DISARMED
|
||||||
return state
|
return state
|
||||||
|
|
||||||
|
|
@ -132,31 +151,35 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
|
||||||
def async_alarm_disarm(self, code=None):
|
def async_alarm_disarm(self, code=None):
|
||||||
"""Send disarm command."""
|
"""Send disarm command."""
|
||||||
if code:
|
if code:
|
||||||
self.hass.data[DATA_EVL].disarm_partition(
|
self.hass.data[DATA_EVL].disarm_partition(str(code), self._partition_number)
|
||||||
str(code), self._partition_number)
|
|
||||||
else:
|
else:
|
||||||
self.hass.data[DATA_EVL].disarm_partition(
|
self.hass.data[DATA_EVL].disarm_partition(
|
||||||
str(self._code), self._partition_number)
|
str(self._code), self._partition_number
|
||||||
|
)
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_alarm_arm_home(self, code=None):
|
def async_alarm_arm_home(self, code=None):
|
||||||
"""Send arm home command."""
|
"""Send arm home command."""
|
||||||
if code:
|
if code:
|
||||||
self.hass.data[DATA_EVL].arm_stay_partition(
|
self.hass.data[DATA_EVL].arm_stay_partition(
|
||||||
str(code), self._partition_number)
|
str(code), self._partition_number
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
self.hass.data[DATA_EVL].arm_stay_partition(
|
self.hass.data[DATA_EVL].arm_stay_partition(
|
||||||
str(self._code), self._partition_number)
|
str(self._code), self._partition_number
|
||||||
|
)
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_alarm_arm_away(self, code=None):
|
def async_alarm_arm_away(self, code=None):
|
||||||
"""Send arm away command."""
|
"""Send arm away command."""
|
||||||
if code:
|
if code:
|
||||||
self.hass.data[DATA_EVL].arm_away_partition(
|
self.hass.data[DATA_EVL].arm_away_partition(
|
||||||
str(code), self._partition_number)
|
str(code), self._partition_number
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
self.hass.data[DATA_EVL].arm_away_partition(
|
self.hass.data[DATA_EVL].arm_away_partition(
|
||||||
str(self._code), self._partition_number)
|
str(self._code), self._partition_number
|
||||||
|
)
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_alarm_trigger(self, code=None):
|
def async_alarm_trigger(self, code=None):
|
||||||
|
|
@ -168,4 +191,5 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
|
||||||
"""Send custom keypress."""
|
"""Send custom keypress."""
|
||||||
if keypress:
|
if keypress:
|
||||||
self.hass.data[DATA_EVL].keypresses_to_partition(
|
self.hass.data[DATA_EVL].keypresses_to_partition(
|
||||||
self._partition_number, keypress)
|
self._partition_number, keypress
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -9,22 +9,26 @@ import logging
|
||||||
|
|
||||||
from homeassistant.components.alarm_control_panel import AlarmControlPanel
|
from homeassistant.components.alarm_control_panel import AlarmControlPanel
|
||||||
from homeassistant.components.homematicip_cloud import (
|
from homeassistant.components.homematicip_cloud import (
|
||||||
HMIPC_HAPID, HomematicipGenericDevice)
|
HMIPC_HAPID,
|
||||||
|
HomematicipGenericDevice,
|
||||||
|
)
|
||||||
from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN
|
from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
|
STATE_ALARM_ARMED_AWAY,
|
||||||
STATE_ALARM_TRIGGERED)
|
STATE_ALARM_ARMED_HOME,
|
||||||
|
STATE_ALARM_DISARMED,
|
||||||
|
STATE_ALARM_TRIGGERED,
|
||||||
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DEPENDENCIES = ['homematicip_cloud']
|
DEPENDENCIES = ["homematicip_cloud"]
|
||||||
|
|
||||||
HMIP_ZONE_AWAY = 'EXTERNAL'
|
HMIP_ZONE_AWAY = "EXTERNAL"
|
||||||
HMIP_ZONE_HOME = 'INTERNAL'
|
HMIP_ZONE_HOME = "INTERNAL"
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_platform(
|
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||||
hass, config, async_add_entities, discovery_info=None):
|
|
||||||
"""Set up the HomematicIP Cloud alarm control devices."""
|
"""Set up the HomematicIP Cloud alarm control devices."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
@ -48,8 +52,8 @@ class HomematicipSecurityZone(HomematicipGenericDevice, AlarmControlPanel):
|
||||||
|
|
||||||
def __init__(self, home, device):
|
def __init__(self, home, device):
|
||||||
"""Initialize the security zone group."""
|
"""Initialize the security zone group."""
|
||||||
device.modelType = 'Group-SecurityZone'
|
device.modelType = "Group-SecurityZone"
|
||||||
device.windowState = ''
|
device.windowState = ""
|
||||||
super().__init__(home, device)
|
super().__init__(home, device)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
@ -58,8 +62,11 @@ class HomematicipSecurityZone(HomematicipGenericDevice, AlarmControlPanel):
|
||||||
from homematicip.base.enums import WindowState
|
from homematicip.base.enums import WindowState
|
||||||
|
|
||||||
if self._device.active:
|
if self._device.active:
|
||||||
if (self._device.sabotage or self._device.motionDetected or
|
if (
|
||||||
self._device.windowState == WindowState.OPEN):
|
self._device.sabotage
|
||||||
|
or self._device.motionDetected
|
||||||
|
or self._device.windowState == WindowState.OPEN
|
||||||
|
):
|
||||||
return STATE_ALARM_TRIGGERED
|
return STATE_ALARM_TRIGGERED
|
||||||
|
|
||||||
active = self._home.get_security_zones_activation()
|
active = self._home.get_security_zones_activation()
|
||||||
|
|
|
||||||
|
|
@ -11,33 +11,40 @@ import voluptuous as vol
|
||||||
import homeassistant.components.alarm_control_panel as alarm
|
import homeassistant.components.alarm_control_panel as alarm
|
||||||
from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
|
from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME, STATE_ALARM_ARMED_AWAY,
|
CONF_HOST,
|
||||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED)
|
CONF_NAME,
|
||||||
|
CONF_PASSWORD,
|
||||||
|
CONF_USERNAME,
|
||||||
|
STATE_ALARM_ARMED_AWAY,
|
||||||
|
STATE_ALARM_ARMED_HOME,
|
||||||
|
STATE_ALARM_DISARMED,
|
||||||
|
)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
REQUIREMENTS = ['pyialarm==0.2']
|
REQUIREMENTS = ["pyialarm==0.2"]
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DEFAULT_NAME = 'iAlarm'
|
DEFAULT_NAME = "iAlarm"
|
||||||
|
|
||||||
|
|
||||||
def no_application_protocol(value):
|
def no_application_protocol(value):
|
||||||
"""Validate that value is without the application protocol."""
|
"""Validate that value is without the application protocol."""
|
||||||
protocol_separator = "://"
|
protocol_separator = "://"
|
||||||
if not value or protocol_separator in value:
|
if not value or protocol_separator in value:
|
||||||
raise vol.Invalid(
|
raise vol.Invalid("Invalid host, {} is not allowed".format(protocol_separator))
|
||||||
'Invalid host, {} is not allowed'.format(protocol_separator))
|
|
||||||
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||||
vol.Required(CONF_HOST): vol.All(cv.string, no_application_protocol),
|
{
|
||||||
vol.Required(CONF_PASSWORD): cv.string,
|
vol.Required(CONF_HOST): vol.All(cv.string, no_application_protocol),
|
||||||
vol.Required(CONF_USERNAME): cv.string,
|
vol.Required(CONF_PASSWORD): cv.string,
|
||||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
vol.Required(CONF_USERNAME): cv.string,
|
||||||
})
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
|
|
@ -47,7 +54,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
password = config.get(CONF_PASSWORD)
|
password = config.get(CONF_PASSWORD)
|
||||||
host = config.get(CONF_HOST)
|
host = config.get(CONF_HOST)
|
||||||
|
|
||||||
url = 'http://{}'.format(host)
|
url = "http://{}".format(host)
|
||||||
ialarm = IAlarmPanel(name, username, password, url)
|
ialarm = IAlarmPanel(name, username, password, url)
|
||||||
add_entities([ialarm], True)
|
add_entities([ialarm], True)
|
||||||
|
|
||||||
|
|
@ -79,7 +86,7 @@ class IAlarmPanel(alarm.AlarmControlPanel):
|
||||||
def update(self):
|
def update(self):
|
||||||
"""Return the state of the device."""
|
"""Return the state of the device."""
|
||||||
status = self._client.get_status()
|
status = self._client.get_status()
|
||||||
_LOGGER.debug('iAlarm status: %s', status)
|
_LOGGER.debug("iAlarm status: %s", status)
|
||||||
if status:
|
if status:
|
||||||
status = int(status)
|
status = int(status)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,25 +10,37 @@ import re
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
import homeassistant.components.alarm_control_panel as alarm
|
import homeassistant.components.alarm_control_panel as alarm
|
||||||
from homeassistant.components.alarm_control_panel import (
|
from homeassistant.components.alarm_control_panel import DOMAIN, PLATFORM_SCHEMA
|
||||||
DOMAIN, PLATFORM_SCHEMA)
|
|
||||||
from homeassistant.components.ifttt import (
|
from homeassistant.components.ifttt import (
|
||||||
ATTR_EVENT, DOMAIN as IFTTT_DOMAIN, SERVICE_TRIGGER)
|
ATTR_EVENT,
|
||||||
|
DOMAIN as IFTTT_DOMAIN,
|
||||||
|
SERVICE_TRIGGER,
|
||||||
|
)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ENTITY_ID, ATTR_STATE, CONF_NAME, CONF_CODE,
|
ATTR_ENTITY_ID,
|
||||||
CONF_OPTIMISTIC, STATE_ALARM_DISARMED, STATE_ALARM_ARMED_NIGHT,
|
ATTR_STATE,
|
||||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY)
|
CONF_NAME,
|
||||||
|
CONF_CODE,
|
||||||
|
CONF_OPTIMISTIC,
|
||||||
|
STATE_ALARM_DISARMED,
|
||||||
|
STATE_ALARM_ARMED_NIGHT,
|
||||||
|
STATE_ALARM_ARMED_HOME,
|
||||||
|
STATE_ALARM_ARMED_AWAY,
|
||||||
|
)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
DEPENDENCIES = ['ifttt']
|
DEPENDENCIES = ["ifttt"]
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
ALLOWED_STATES = [
|
ALLOWED_STATES = [
|
||||||
STATE_ALARM_DISARMED, STATE_ALARM_ARMED_NIGHT,
|
STATE_ALARM_DISARMED,
|
||||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME]
|
STATE_ALARM_ARMED_NIGHT,
|
||||||
|
STATE_ALARM_ARMED_AWAY,
|
||||||
|
STATE_ALARM_ARMED_HOME,
|
||||||
|
]
|
||||||
|
|
||||||
DATA_IFTTT_ALARM = 'ifttt_alarm'
|
DATA_IFTTT_ALARM = "ifttt_alarm"
|
||||||
DEFAULT_NAME = "Home"
|
DEFAULT_NAME = "Home"
|
||||||
|
|
||||||
CONF_EVENT_AWAY = "event_arm_away"
|
CONF_EVENT_AWAY = "event_arm_away"
|
||||||
|
|
@ -41,22 +53,23 @@ DEFAULT_EVENT_HOME = "alarm_arm_home"
|
||||||
DEFAULT_EVENT_NIGHT = "alarm_arm_night"
|
DEFAULT_EVENT_NIGHT = "alarm_arm_night"
|
||||||
DEFAULT_EVENT_DISARM = "alarm_disarm"
|
DEFAULT_EVENT_DISARM = "alarm_disarm"
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
{
|
||||||
vol.Optional(CONF_CODE): cv.string,
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||||
vol.Optional(CONF_EVENT_AWAY, default=DEFAULT_EVENT_AWAY): cv.string,
|
vol.Optional(CONF_CODE): cv.string,
|
||||||
vol.Optional(CONF_EVENT_HOME, default=DEFAULT_EVENT_HOME): cv.string,
|
vol.Optional(CONF_EVENT_AWAY, default=DEFAULT_EVENT_AWAY): cv.string,
|
||||||
vol.Optional(CONF_EVENT_NIGHT, default=DEFAULT_EVENT_NIGHT): cv.string,
|
vol.Optional(CONF_EVENT_HOME, default=DEFAULT_EVENT_HOME): cv.string,
|
||||||
vol.Optional(CONF_EVENT_DISARM, default=DEFAULT_EVENT_DISARM): cv.string,
|
vol.Optional(CONF_EVENT_NIGHT, default=DEFAULT_EVENT_NIGHT): cv.string,
|
||||||
vol.Optional(CONF_OPTIMISTIC, default=False): cv.boolean,
|
vol.Optional(CONF_EVENT_DISARM, default=DEFAULT_EVENT_DISARM): cv.string,
|
||||||
})
|
vol.Optional(CONF_OPTIMISTIC, default=False): cv.boolean,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
SERVICE_PUSH_ALARM_STATE = "ifttt_push_alarm_state"
|
SERVICE_PUSH_ALARM_STATE = "ifttt_push_alarm_state"
|
||||||
|
|
||||||
PUSH_ALARM_STATE_SERVICE_SCHEMA = vol.Schema({
|
PUSH_ALARM_STATE_SERVICE_SCHEMA = vol.Schema(
|
||||||
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
|
{vol.Required(ATTR_ENTITY_ID): cv.entity_ids, vol.Required(ATTR_STATE): cv.string}
|
||||||
vol.Required(ATTR_STATE): cv.string,
|
)
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
|
|
@ -72,8 +85,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
event_disarm = config.get(CONF_EVENT_DISARM)
|
event_disarm = config.get(CONF_EVENT_DISARM)
|
||||||
optimistic = config.get(CONF_OPTIMISTIC)
|
optimistic = config.get(CONF_OPTIMISTIC)
|
||||||
|
|
||||||
alarmpanel = IFTTTAlarmPanel(name, code, event_away, event_home,
|
alarmpanel = IFTTTAlarmPanel(
|
||||||
event_night, event_disarm, optimistic)
|
name, code, event_away, event_home, event_night, event_disarm, optimistic
|
||||||
|
)
|
||||||
hass.data[DATA_IFTTT_ALARM].append(alarmpanel)
|
hass.data[DATA_IFTTT_ALARM].append(alarmpanel)
|
||||||
add_entities([alarmpanel])
|
add_entities([alarmpanel])
|
||||||
|
|
||||||
|
|
@ -89,15 +103,20 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
device.push_alarm_state(state)
|
device.push_alarm_state(state)
|
||||||
device.async_schedule_update_ha_state()
|
device.async_schedule_update_ha_state()
|
||||||
|
|
||||||
hass.services.register(DOMAIN, SERVICE_PUSH_ALARM_STATE, push_state_update,
|
hass.services.register(
|
||||||
schema=PUSH_ALARM_STATE_SERVICE_SCHEMA)
|
DOMAIN,
|
||||||
|
SERVICE_PUSH_ALARM_STATE,
|
||||||
|
push_state_update,
|
||||||
|
schema=PUSH_ALARM_STATE_SERVICE_SCHEMA,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class IFTTTAlarmPanel(alarm.AlarmControlPanel):
|
class IFTTTAlarmPanel(alarm.AlarmControlPanel):
|
||||||
"""Representation of an alarm control panel controlled through IFTTT."""
|
"""Representation of an alarm control panel controlled through IFTTT."""
|
||||||
|
|
||||||
def __init__(self, name, code, event_away, event_home, event_night,
|
def __init__(
|
||||||
event_disarm, optimistic):
|
self, name, code, event_away, event_home, event_night, event_disarm, optimistic
|
||||||
|
):
|
||||||
"""Initialize the alarm control panel."""
|
"""Initialize the alarm control panel."""
|
||||||
self._name = name
|
self._name = name
|
||||||
self._code = code
|
self._code = code
|
||||||
|
|
@ -128,9 +147,9 @@ class IFTTTAlarmPanel(alarm.AlarmControlPanel):
|
||||||
"""Return one or more digits/characters."""
|
"""Return one or more digits/characters."""
|
||||||
if self._code is None:
|
if self._code is None:
|
||||||
return None
|
return None
|
||||||
if isinstance(self._code, str) and re.search('^\\d+$', self._code):
|
if isinstance(self._code, str) and re.search("^\\d+$", self._code):
|
||||||
return 'Number'
|
return "Number"
|
||||||
return 'Any'
|
return "Any"
|
||||||
|
|
||||||
def alarm_disarm(self, code=None):
|
def alarm_disarm(self, code=None):
|
||||||
"""Send disarm command."""
|
"""Send disarm command."""
|
||||||
|
|
|
||||||
|
|
@ -13,37 +13,54 @@ import voluptuous as vol
|
||||||
|
|
||||||
import homeassistant.components.alarm_control_panel as alarm
|
import homeassistant.components.alarm_control_panel as alarm
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_CODE, CONF_DELAY_TIME, CONF_DISARM_AFTER_TRIGGER, CONF_NAME,
|
CONF_CODE,
|
||||||
CONF_PENDING_TIME, CONF_PLATFORM, CONF_TRIGGER_TIME,
|
CONF_DELAY_TIME,
|
||||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_CUSTOM_BYPASS,
|
CONF_DISARM_AFTER_TRIGGER,
|
||||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED,
|
CONF_NAME,
|
||||||
STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED)
|
CONF_PENDING_TIME,
|
||||||
|
CONF_PLATFORM,
|
||||||
|
CONF_TRIGGER_TIME,
|
||||||
|
STATE_ALARM_ARMED_AWAY,
|
||||||
|
STATE_ALARM_ARMED_CUSTOM_BYPASS,
|
||||||
|
STATE_ALARM_ARMED_HOME,
|
||||||
|
STATE_ALARM_ARMED_NIGHT,
|
||||||
|
STATE_ALARM_DISARMED,
|
||||||
|
STATE_ALARM_PENDING,
|
||||||
|
STATE_ALARM_TRIGGERED,
|
||||||
|
)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.event import track_point_in_time
|
from homeassistant.helpers.event import track_point_in_time
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
CONF_CODE_TEMPLATE = 'code_template'
|
CONF_CODE_TEMPLATE = "code_template"
|
||||||
|
|
||||||
DEFAULT_ALARM_NAME = 'HA Alarm'
|
DEFAULT_ALARM_NAME = "HA Alarm"
|
||||||
DEFAULT_DELAY_TIME = datetime.timedelta(seconds=0)
|
DEFAULT_DELAY_TIME = datetime.timedelta(seconds=0)
|
||||||
DEFAULT_PENDING_TIME = datetime.timedelta(seconds=60)
|
DEFAULT_PENDING_TIME = datetime.timedelta(seconds=60)
|
||||||
DEFAULT_TRIGGER_TIME = datetime.timedelta(seconds=120)
|
DEFAULT_TRIGGER_TIME = datetime.timedelta(seconds=120)
|
||||||
DEFAULT_DISARM_AFTER_TRIGGER = False
|
DEFAULT_DISARM_AFTER_TRIGGER = False
|
||||||
|
|
||||||
SUPPORTED_STATES = [STATE_ALARM_DISARMED, STATE_ALARM_ARMED_AWAY,
|
SUPPORTED_STATES = [
|
||||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT,
|
STATE_ALARM_DISARMED,
|
||||||
STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_TRIGGERED]
|
STATE_ALARM_ARMED_AWAY,
|
||||||
|
STATE_ALARM_ARMED_HOME,
|
||||||
|
STATE_ALARM_ARMED_NIGHT,
|
||||||
|
STATE_ALARM_ARMED_CUSTOM_BYPASS,
|
||||||
|
STATE_ALARM_TRIGGERED,
|
||||||
|
]
|
||||||
|
|
||||||
SUPPORTED_PRETRIGGER_STATES = [state for state in SUPPORTED_STATES
|
SUPPORTED_PRETRIGGER_STATES = [
|
||||||
if state != STATE_ALARM_TRIGGERED]
|
state for state in SUPPORTED_STATES if state != STATE_ALARM_TRIGGERED
|
||||||
|
]
|
||||||
|
|
||||||
SUPPORTED_PENDING_STATES = [state for state in SUPPORTED_STATES
|
SUPPORTED_PENDING_STATES = [
|
||||||
if state != STATE_ALARM_DISARMED]
|
state for state in SUPPORTED_STATES if state != STATE_ALARM_DISARMED
|
||||||
|
]
|
||||||
|
|
||||||
ATTR_PRE_PENDING_STATE = 'pre_pending_state'
|
ATTR_PRE_PENDING_STATE = "pre_pending_state"
|
||||||
ATTR_POST_PENDING_STATE = 'post_pending_state'
|
ATTR_POST_PENDING_STATE = "post_pending_state"
|
||||||
|
|
||||||
|
|
||||||
def _state_validator(config):
|
def _state_validator(config):
|
||||||
|
|
@ -66,53 +83,75 @@ def _state_schema(state):
|
||||||
schema = {}
|
schema = {}
|
||||||
if state in SUPPORTED_PRETRIGGER_STATES:
|
if state in SUPPORTED_PRETRIGGER_STATES:
|
||||||
schema[vol.Optional(CONF_DELAY_TIME)] = vol.All(
|
schema[vol.Optional(CONF_DELAY_TIME)] = vol.All(
|
||||||
cv.time_period, cv.positive_timedelta)
|
cv.time_period, cv.positive_timedelta
|
||||||
|
)
|
||||||
schema[vol.Optional(CONF_TRIGGER_TIME)] = vol.All(
|
schema[vol.Optional(CONF_TRIGGER_TIME)] = vol.All(
|
||||||
cv.time_period, cv.positive_timedelta)
|
cv.time_period, cv.positive_timedelta
|
||||||
|
)
|
||||||
if state in SUPPORTED_PENDING_STATES:
|
if state in SUPPORTED_PENDING_STATES:
|
||||||
schema[vol.Optional(CONF_PENDING_TIME)] = vol.All(
|
schema[vol.Optional(CONF_PENDING_TIME)] = vol.All(
|
||||||
cv.time_period, cv.positive_timedelta)
|
cv.time_period, cv.positive_timedelta
|
||||||
|
)
|
||||||
return vol.Schema(schema)
|
return vol.Schema(schema)
|
||||||
|
|
||||||
|
|
||||||
PLATFORM_SCHEMA = vol.Schema(vol.All({
|
PLATFORM_SCHEMA = vol.Schema(
|
||||||
vol.Required(CONF_PLATFORM): 'manual',
|
vol.All(
|
||||||
vol.Optional(CONF_NAME, default=DEFAULT_ALARM_NAME): cv.string,
|
{
|
||||||
vol.Exclusive(CONF_CODE, 'code validation'): cv.string,
|
vol.Required(CONF_PLATFORM): "manual",
|
||||||
vol.Exclusive(CONF_CODE_TEMPLATE, 'code validation'): cv.template,
|
vol.Optional(CONF_NAME, default=DEFAULT_ALARM_NAME): cv.string,
|
||||||
vol.Optional(CONF_DELAY_TIME, default=DEFAULT_DELAY_TIME):
|
vol.Exclusive(CONF_CODE, "code validation"): cv.string,
|
||||||
vol.All(cv.time_period, cv.positive_timedelta),
|
vol.Exclusive(CONF_CODE_TEMPLATE, "code validation"): cv.template,
|
||||||
vol.Optional(CONF_PENDING_TIME, default=DEFAULT_PENDING_TIME):
|
vol.Optional(CONF_DELAY_TIME, default=DEFAULT_DELAY_TIME): vol.All(
|
||||||
vol.All(cv.time_period, cv.positive_timedelta),
|
cv.time_period, cv.positive_timedelta
|
||||||
vol.Optional(CONF_TRIGGER_TIME, default=DEFAULT_TRIGGER_TIME):
|
),
|
||||||
vol.All(cv.time_period, cv.positive_timedelta),
|
vol.Optional(CONF_PENDING_TIME, default=DEFAULT_PENDING_TIME): vol.All(
|
||||||
vol.Optional(CONF_DISARM_AFTER_TRIGGER,
|
cv.time_period, cv.positive_timedelta
|
||||||
default=DEFAULT_DISARM_AFTER_TRIGGER): cv.boolean,
|
),
|
||||||
vol.Optional(STATE_ALARM_ARMED_AWAY, default={}):
|
vol.Optional(CONF_TRIGGER_TIME, default=DEFAULT_TRIGGER_TIME): vol.All(
|
||||||
_state_schema(STATE_ALARM_ARMED_AWAY),
|
cv.time_period, cv.positive_timedelta
|
||||||
vol.Optional(STATE_ALARM_ARMED_HOME, default={}):
|
),
|
||||||
_state_schema(STATE_ALARM_ARMED_HOME),
|
vol.Optional(
|
||||||
vol.Optional(STATE_ALARM_ARMED_NIGHT, default={}):
|
CONF_DISARM_AFTER_TRIGGER, default=DEFAULT_DISARM_AFTER_TRIGGER
|
||||||
_state_schema(STATE_ALARM_ARMED_NIGHT),
|
): cv.boolean,
|
||||||
vol.Optional(STATE_ALARM_ARMED_CUSTOM_BYPASS, default={}):
|
vol.Optional(STATE_ALARM_ARMED_AWAY, default={}): _state_schema(
|
||||||
_state_schema(STATE_ALARM_ARMED_CUSTOM_BYPASS),
|
STATE_ALARM_ARMED_AWAY
|
||||||
vol.Optional(STATE_ALARM_DISARMED, default={}):
|
),
|
||||||
_state_schema(STATE_ALARM_DISARMED),
|
vol.Optional(STATE_ALARM_ARMED_HOME, default={}): _state_schema(
|
||||||
vol.Optional(STATE_ALARM_TRIGGERED, default={}):
|
STATE_ALARM_ARMED_HOME
|
||||||
_state_schema(STATE_ALARM_TRIGGERED),
|
),
|
||||||
}, _state_validator))
|
vol.Optional(STATE_ALARM_ARMED_NIGHT, default={}): _state_schema(
|
||||||
|
STATE_ALARM_ARMED_NIGHT
|
||||||
|
),
|
||||||
|
vol.Optional(STATE_ALARM_ARMED_CUSTOM_BYPASS, default={}): _state_schema(
|
||||||
|
STATE_ALARM_ARMED_CUSTOM_BYPASS
|
||||||
|
),
|
||||||
|
vol.Optional(STATE_ALARM_DISARMED, default={}): _state_schema(
|
||||||
|
STATE_ALARM_DISARMED
|
||||||
|
),
|
||||||
|
vol.Optional(STATE_ALARM_TRIGGERED, default={}): _state_schema(
|
||||||
|
STATE_ALARM_TRIGGERED
|
||||||
|
),
|
||||||
|
},
|
||||||
|
_state_validator,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
"""Set up the manual alarm platform."""
|
"""Set up the manual alarm platform."""
|
||||||
add_entities([ManualAlarm(
|
add_entities(
|
||||||
hass,
|
[
|
||||||
config[CONF_NAME],
|
ManualAlarm(
|
||||||
config.get(CONF_CODE),
|
hass,
|
||||||
config.get(CONF_CODE_TEMPLATE),
|
config[CONF_NAME],
|
||||||
config.get(CONF_DISARM_AFTER_TRIGGER, DEFAULT_DISARM_AFTER_TRIGGER),
|
config.get(CONF_CODE),
|
||||||
config
|
config.get(CONF_CODE_TEMPLATE),
|
||||||
)])
|
config.get(CONF_DISARM_AFTER_TRIGGER, DEFAULT_DISARM_AFTER_TRIGGER),
|
||||||
|
config,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ManualAlarm(alarm.AlarmControlPanel):
|
class ManualAlarm(alarm.AlarmControlPanel):
|
||||||
|
|
@ -127,8 +166,7 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
||||||
A trigger_time of zero disables the alarm_trigger service.
|
A trigger_time of zero disables the alarm_trigger service.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, hass, name, code, code_template,
|
def __init__(self, hass, name, code, code_template, disarm_after_trigger, config):
|
||||||
disarm_after_trigger, config):
|
|
||||||
"""Init the manual alarm panel."""
|
"""Init the manual alarm panel."""
|
||||||
self._state = STATE_ALARM_DISARMED
|
self._state = STATE_ALARM_DISARMED
|
||||||
self._hass = hass
|
self._hass = hass
|
||||||
|
|
@ -144,13 +182,16 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
||||||
|
|
||||||
self._delay_time_by_state = {
|
self._delay_time_by_state = {
|
||||||
state: config[state][CONF_DELAY_TIME]
|
state: config[state][CONF_DELAY_TIME]
|
||||||
for state in SUPPORTED_PRETRIGGER_STATES}
|
for state in SUPPORTED_PRETRIGGER_STATES
|
||||||
|
}
|
||||||
self._trigger_time_by_state = {
|
self._trigger_time_by_state = {
|
||||||
state: config[state][CONF_TRIGGER_TIME]
|
state: config[state][CONF_TRIGGER_TIME]
|
||||||
for state in SUPPORTED_PRETRIGGER_STATES}
|
for state in SUPPORTED_PRETRIGGER_STATES
|
||||||
|
}
|
||||||
self._pending_time_by_state = {
|
self._pending_time_by_state = {
|
||||||
state: config[state][CONF_PENDING_TIME]
|
state: config[state][CONF_PENDING_TIME]
|
||||||
for state in SUPPORTED_PENDING_STATES}
|
for state in SUPPORTED_PENDING_STATES
|
||||||
|
}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def should_poll(self):
|
def should_poll(self):
|
||||||
|
|
@ -169,15 +210,17 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
||||||
if self._within_pending_time(self._state):
|
if self._within_pending_time(self._state):
|
||||||
return STATE_ALARM_PENDING
|
return STATE_ALARM_PENDING
|
||||||
trigger_time = self._trigger_time_by_state[self._previous_state]
|
trigger_time = self._trigger_time_by_state[self._previous_state]
|
||||||
if (self._state_ts + self._pending_time(self._state) +
|
if (
|
||||||
trigger_time) < dt_util.utcnow():
|
self._state_ts + self._pending_time(self._state) + trigger_time
|
||||||
|
) < dt_util.utcnow():
|
||||||
if self._disarm_after_trigger:
|
if self._disarm_after_trigger:
|
||||||
return STATE_ALARM_DISARMED
|
return STATE_ALARM_DISARMED
|
||||||
self._state = self._previous_state
|
self._state = self._previous_state
|
||||||
return self._state
|
return self._state
|
||||||
|
|
||||||
if self._state in SUPPORTED_PENDING_STATES and \
|
if self._state in SUPPORTED_PENDING_STATES and self._within_pending_time(
|
||||||
self._within_pending_time(self._state):
|
self._state
|
||||||
|
):
|
||||||
return STATE_ALARM_PENDING
|
return STATE_ALARM_PENDING
|
||||||
|
|
||||||
return self._state
|
return self._state
|
||||||
|
|
@ -205,9 +248,9 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
||||||
"""Return one or more digits/characters."""
|
"""Return one or more digits/characters."""
|
||||||
if self._code is None:
|
if self._code is None:
|
||||||
return None
|
return None
|
||||||
if isinstance(self._code, str) and re.search('^\\d+$', self._code):
|
if isinstance(self._code, str) and re.search("^\\d+$", self._code):
|
||||||
return 'Number'
|
return "Number"
|
||||||
return 'Any'
|
return "Any"
|
||||||
|
|
||||||
def alarm_disarm(self, code=None):
|
def alarm_disarm(self, code=None):
|
||||||
"""Send disarm command."""
|
"""Send disarm command."""
|
||||||
|
|
@ -270,17 +313,19 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
||||||
pending_time = self._pending_time(state)
|
pending_time = self._pending_time(state)
|
||||||
if state == STATE_ALARM_TRIGGERED:
|
if state == STATE_ALARM_TRIGGERED:
|
||||||
track_point_in_time(
|
track_point_in_time(
|
||||||
self._hass, self.async_update_ha_state,
|
self._hass, self.async_update_ha_state, self._state_ts + pending_time
|
||||||
self._state_ts + pending_time)
|
)
|
||||||
|
|
||||||
trigger_time = self._trigger_time_by_state[self._previous_state]
|
trigger_time = self._trigger_time_by_state[self._previous_state]
|
||||||
track_point_in_time(
|
track_point_in_time(
|
||||||
self._hass, self.async_update_ha_state,
|
self._hass,
|
||||||
self._state_ts + pending_time + trigger_time)
|
self.async_update_ha_state,
|
||||||
|
self._state_ts + pending_time + trigger_time,
|
||||||
|
)
|
||||||
elif state in SUPPORTED_PENDING_STATES and pending_time:
|
elif state in SUPPORTED_PENDING_STATES and pending_time:
|
||||||
track_point_in_time(
|
track_point_in_time(
|
||||||
self._hass, self.async_update_ha_state,
|
self._hass, self.async_update_ha_state, self._state_ts + pending_time
|
||||||
self._state_ts + pending_time)
|
)
|
||||||
|
|
||||||
def _validate_code(self, code, state):
|
def _validate_code(self, code, state):
|
||||||
"""Validate given code."""
|
"""Validate given code."""
|
||||||
|
|
@ -289,8 +334,7 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
||||||
if isinstance(self._code, str):
|
if isinstance(self._code, str):
|
||||||
alarm_code = self._code
|
alarm_code = self._code
|
||||||
else:
|
else:
|
||||||
alarm_code = self._code.render(from_state=self._state,
|
alarm_code = self._code.render(from_state=self._state, to_state=state)
|
||||||
to_state=state)
|
|
||||||
check = not alarm_code or code == alarm_code
|
check = not alarm_code or code == alarm_code
|
||||||
if not check:
|
if not check:
|
||||||
_LOGGER.warning("Invalid code given for %s", state)
|
_LOGGER.warning("Invalid code given for %s", state)
|
||||||
|
|
|
||||||
|
|
@ -15,10 +15,20 @@ import voluptuous as vol
|
||||||
import homeassistant.components.alarm_control_panel as alarm
|
import homeassistant.components.alarm_control_panel as alarm
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT,
|
STATE_ALARM_ARMED_AWAY,
|
||||||
STATE_ALARM_DISARMED, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED,
|
STATE_ALARM_ARMED_HOME,
|
||||||
CONF_PLATFORM, CONF_NAME, CONF_CODE, CONF_DELAY_TIME, CONF_PENDING_TIME,
|
STATE_ALARM_ARMED_NIGHT,
|
||||||
CONF_TRIGGER_TIME, CONF_DISARM_AFTER_TRIGGER)
|
STATE_ALARM_DISARMED,
|
||||||
|
STATE_ALARM_PENDING,
|
||||||
|
STATE_ALARM_TRIGGERED,
|
||||||
|
CONF_PLATFORM,
|
||||||
|
CONF_NAME,
|
||||||
|
CONF_CODE,
|
||||||
|
CONF_DELAY_TIME,
|
||||||
|
CONF_PENDING_TIME,
|
||||||
|
CONF_TRIGGER_TIME,
|
||||||
|
CONF_DISARM_AFTER_TRIGGER,
|
||||||
|
)
|
||||||
from homeassistant.components import mqtt
|
from homeassistant.components import mqtt
|
||||||
|
|
||||||
from homeassistant.helpers.event import async_track_state_change
|
from homeassistant.helpers.event import async_track_state_change
|
||||||
|
|
@ -29,35 +39,41 @@ from homeassistant.helpers.event import track_point_in_time
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
CONF_CODE_TEMPLATE = 'code_template'
|
CONF_CODE_TEMPLATE = "code_template"
|
||||||
|
|
||||||
CONF_PAYLOAD_DISARM = 'payload_disarm'
|
CONF_PAYLOAD_DISARM = "payload_disarm"
|
||||||
CONF_PAYLOAD_ARM_HOME = 'payload_arm_home'
|
CONF_PAYLOAD_ARM_HOME = "payload_arm_home"
|
||||||
CONF_PAYLOAD_ARM_AWAY = 'payload_arm_away'
|
CONF_PAYLOAD_ARM_AWAY = "payload_arm_away"
|
||||||
CONF_PAYLOAD_ARM_NIGHT = 'payload_arm_night'
|
CONF_PAYLOAD_ARM_NIGHT = "payload_arm_night"
|
||||||
|
|
||||||
DEFAULT_ALARM_NAME = 'HA Alarm'
|
DEFAULT_ALARM_NAME = "HA Alarm"
|
||||||
DEFAULT_DELAY_TIME = datetime.timedelta(seconds=0)
|
DEFAULT_DELAY_TIME = datetime.timedelta(seconds=0)
|
||||||
DEFAULT_PENDING_TIME = datetime.timedelta(seconds=60)
|
DEFAULT_PENDING_TIME = datetime.timedelta(seconds=60)
|
||||||
DEFAULT_TRIGGER_TIME = datetime.timedelta(seconds=120)
|
DEFAULT_TRIGGER_TIME = datetime.timedelta(seconds=120)
|
||||||
DEFAULT_DISARM_AFTER_TRIGGER = False
|
DEFAULT_DISARM_AFTER_TRIGGER = False
|
||||||
DEFAULT_ARM_AWAY = 'ARM_AWAY'
|
DEFAULT_ARM_AWAY = "ARM_AWAY"
|
||||||
DEFAULT_ARM_HOME = 'ARM_HOME'
|
DEFAULT_ARM_HOME = "ARM_HOME"
|
||||||
DEFAULT_ARM_NIGHT = 'ARM_NIGHT'
|
DEFAULT_ARM_NIGHT = "ARM_NIGHT"
|
||||||
DEFAULT_DISARM = 'DISARM'
|
DEFAULT_DISARM = "DISARM"
|
||||||
|
|
||||||
SUPPORTED_STATES = [STATE_ALARM_DISARMED, STATE_ALARM_ARMED_AWAY,
|
SUPPORTED_STATES = [
|
||||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT,
|
STATE_ALARM_DISARMED,
|
||||||
STATE_ALARM_TRIGGERED]
|
STATE_ALARM_ARMED_AWAY,
|
||||||
|
STATE_ALARM_ARMED_HOME,
|
||||||
|
STATE_ALARM_ARMED_NIGHT,
|
||||||
|
STATE_ALARM_TRIGGERED,
|
||||||
|
]
|
||||||
|
|
||||||
SUPPORTED_PRETRIGGER_STATES = [state for state in SUPPORTED_STATES
|
SUPPORTED_PRETRIGGER_STATES = [
|
||||||
if state != STATE_ALARM_TRIGGERED]
|
state for state in SUPPORTED_STATES if state != STATE_ALARM_TRIGGERED
|
||||||
|
]
|
||||||
|
|
||||||
SUPPORTED_PENDING_STATES = [state for state in SUPPORTED_STATES
|
SUPPORTED_PENDING_STATES = [
|
||||||
if state != STATE_ALARM_DISARMED]
|
state for state in SUPPORTED_STATES if state != STATE_ALARM_DISARMED
|
||||||
|
]
|
||||||
|
|
||||||
ATTR_PRE_PENDING_STATE = 'pre_pending_state'
|
ATTR_PRE_PENDING_STATE = "pre_pending_state"
|
||||||
ATTR_POST_PENDING_STATE = 'post_pending_state'
|
ATTR_POST_PENDING_STATE = "post_pending_state"
|
||||||
|
|
||||||
|
|
||||||
def _state_validator(config):
|
def _state_validator(config):
|
||||||
|
|
@ -80,65 +96,95 @@ def _state_schema(state):
|
||||||
schema = {}
|
schema = {}
|
||||||
if state in SUPPORTED_PRETRIGGER_STATES:
|
if state in SUPPORTED_PRETRIGGER_STATES:
|
||||||
schema[vol.Optional(CONF_DELAY_TIME)] = vol.All(
|
schema[vol.Optional(CONF_DELAY_TIME)] = vol.All(
|
||||||
cv.time_period, cv.positive_timedelta)
|
cv.time_period, cv.positive_timedelta
|
||||||
|
)
|
||||||
schema[vol.Optional(CONF_TRIGGER_TIME)] = vol.All(
|
schema[vol.Optional(CONF_TRIGGER_TIME)] = vol.All(
|
||||||
cv.time_period, cv.positive_timedelta)
|
cv.time_period, cv.positive_timedelta
|
||||||
|
)
|
||||||
if state in SUPPORTED_PENDING_STATES:
|
if state in SUPPORTED_PENDING_STATES:
|
||||||
schema[vol.Optional(CONF_PENDING_TIME)] = vol.All(
|
schema[vol.Optional(CONF_PENDING_TIME)] = vol.All(
|
||||||
cv.time_period, cv.positive_timedelta)
|
cv.time_period, cv.positive_timedelta
|
||||||
|
)
|
||||||
return vol.Schema(schema)
|
return vol.Schema(schema)
|
||||||
|
|
||||||
|
|
||||||
DEPENDENCIES = ['mqtt']
|
DEPENDENCIES = ["mqtt"]
|
||||||
|
|
||||||
PLATFORM_SCHEMA = vol.Schema(vol.All(mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = vol.Schema(
|
||||||
vol.Required(CONF_PLATFORM): 'manual_mqtt',
|
vol.All(
|
||||||
vol.Optional(CONF_NAME, default=DEFAULT_ALARM_NAME): cv.string,
|
mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend(
|
||||||
vol.Exclusive(CONF_CODE, 'code validation'): cv.string,
|
{
|
||||||
vol.Exclusive(CONF_CODE_TEMPLATE, 'code validation'): cv.template,
|
vol.Required(CONF_PLATFORM): "manual_mqtt",
|
||||||
vol.Optional(CONF_DELAY_TIME, default=DEFAULT_DELAY_TIME):
|
vol.Optional(CONF_NAME, default=DEFAULT_ALARM_NAME): cv.string,
|
||||||
vol.All(cv.time_period, cv.positive_timedelta),
|
vol.Exclusive(CONF_CODE, "code validation"): cv.string,
|
||||||
vol.Optional(CONF_PENDING_TIME, default=DEFAULT_PENDING_TIME):
|
vol.Exclusive(CONF_CODE_TEMPLATE, "code validation"): cv.template,
|
||||||
vol.All(cv.time_period, cv.positive_timedelta),
|
vol.Optional(CONF_DELAY_TIME, default=DEFAULT_DELAY_TIME): vol.All(
|
||||||
vol.Optional(CONF_TRIGGER_TIME, default=DEFAULT_TRIGGER_TIME):
|
cv.time_period, cv.positive_timedelta
|
||||||
vol.All(cv.time_period, cv.positive_timedelta),
|
),
|
||||||
vol.Optional(CONF_DISARM_AFTER_TRIGGER,
|
vol.Optional(CONF_PENDING_TIME, default=DEFAULT_PENDING_TIME): vol.All(
|
||||||
default=DEFAULT_DISARM_AFTER_TRIGGER): cv.boolean,
|
cv.time_period, cv.positive_timedelta
|
||||||
vol.Optional(STATE_ALARM_ARMED_AWAY, default={}):
|
),
|
||||||
_state_schema(STATE_ALARM_ARMED_AWAY),
|
vol.Optional(CONF_TRIGGER_TIME, default=DEFAULT_TRIGGER_TIME): vol.All(
|
||||||
vol.Optional(STATE_ALARM_ARMED_HOME, default={}):
|
cv.time_period, cv.positive_timedelta
|
||||||
_state_schema(STATE_ALARM_ARMED_HOME),
|
),
|
||||||
vol.Optional(STATE_ALARM_ARMED_NIGHT, default={}):
|
vol.Optional(
|
||||||
_state_schema(STATE_ALARM_ARMED_NIGHT),
|
CONF_DISARM_AFTER_TRIGGER, default=DEFAULT_DISARM_AFTER_TRIGGER
|
||||||
vol.Optional(STATE_ALARM_DISARMED, default={}):
|
): cv.boolean,
|
||||||
_state_schema(STATE_ALARM_DISARMED),
|
vol.Optional(STATE_ALARM_ARMED_AWAY, default={}): _state_schema(
|
||||||
vol.Optional(STATE_ALARM_TRIGGERED, default={}):
|
STATE_ALARM_ARMED_AWAY
|
||||||
_state_schema(STATE_ALARM_TRIGGERED),
|
),
|
||||||
vol.Required(mqtt.CONF_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
vol.Optional(STATE_ALARM_ARMED_HOME, default={}): _state_schema(
|
||||||
vol.Required(mqtt.CONF_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
STATE_ALARM_ARMED_HOME
|
||||||
vol.Optional(CONF_PAYLOAD_ARM_AWAY, default=DEFAULT_ARM_AWAY): cv.string,
|
),
|
||||||
vol.Optional(CONF_PAYLOAD_ARM_HOME, default=DEFAULT_ARM_HOME): cv.string,
|
vol.Optional(STATE_ALARM_ARMED_NIGHT, default={}): _state_schema(
|
||||||
vol.Optional(CONF_PAYLOAD_ARM_NIGHT, default=DEFAULT_ARM_NIGHT): cv.string,
|
STATE_ALARM_ARMED_NIGHT
|
||||||
vol.Optional(CONF_PAYLOAD_DISARM, default=DEFAULT_DISARM): cv.string,
|
),
|
||||||
}), _state_validator))
|
vol.Optional(STATE_ALARM_DISARMED, default={}): _state_schema(
|
||||||
|
STATE_ALARM_DISARMED
|
||||||
|
),
|
||||||
|
vol.Optional(STATE_ALARM_TRIGGERED, default={}): _state_schema(
|
||||||
|
STATE_ALARM_TRIGGERED
|
||||||
|
),
|
||||||
|
vol.Required(mqtt.CONF_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||||
|
vol.Required(mqtt.CONF_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
||||||
|
vol.Optional(
|
||||||
|
CONF_PAYLOAD_ARM_AWAY, default=DEFAULT_ARM_AWAY
|
||||||
|
): cv.string,
|
||||||
|
vol.Optional(
|
||||||
|
CONF_PAYLOAD_ARM_HOME, default=DEFAULT_ARM_HOME
|
||||||
|
): cv.string,
|
||||||
|
vol.Optional(
|
||||||
|
CONF_PAYLOAD_ARM_NIGHT, default=DEFAULT_ARM_NIGHT
|
||||||
|
): cv.string,
|
||||||
|
vol.Optional(CONF_PAYLOAD_DISARM, default=DEFAULT_DISARM): cv.string,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
_state_validator,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
"""Set up the manual MQTT alarm platform."""
|
"""Set up the manual MQTT alarm platform."""
|
||||||
add_entities([ManualMQTTAlarm(
|
add_entities(
|
||||||
hass,
|
[
|
||||||
config[CONF_NAME],
|
ManualMQTTAlarm(
|
||||||
config.get(CONF_CODE),
|
hass,
|
||||||
config.get(CONF_CODE_TEMPLATE),
|
config[CONF_NAME],
|
||||||
config.get(CONF_DISARM_AFTER_TRIGGER, DEFAULT_DISARM_AFTER_TRIGGER),
|
config.get(CONF_CODE),
|
||||||
config.get(mqtt.CONF_STATE_TOPIC),
|
config.get(CONF_CODE_TEMPLATE),
|
||||||
config.get(mqtt.CONF_COMMAND_TOPIC),
|
config.get(CONF_DISARM_AFTER_TRIGGER, DEFAULT_DISARM_AFTER_TRIGGER),
|
||||||
config.get(mqtt.CONF_QOS),
|
config.get(mqtt.CONF_STATE_TOPIC),
|
||||||
config.get(CONF_PAYLOAD_DISARM),
|
config.get(mqtt.CONF_COMMAND_TOPIC),
|
||||||
config.get(CONF_PAYLOAD_ARM_HOME),
|
config.get(mqtt.CONF_QOS),
|
||||||
config.get(CONF_PAYLOAD_ARM_AWAY),
|
config.get(CONF_PAYLOAD_DISARM),
|
||||||
config.get(CONF_PAYLOAD_ARM_NIGHT),
|
config.get(CONF_PAYLOAD_ARM_HOME),
|
||||||
config)])
|
config.get(CONF_PAYLOAD_ARM_AWAY),
|
||||||
|
config.get(CONF_PAYLOAD_ARM_NIGHT),
|
||||||
|
config,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ManualMQTTAlarm(alarm.AlarmControlPanel):
|
class ManualMQTTAlarm(alarm.AlarmControlPanel):
|
||||||
|
|
@ -153,10 +199,22 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
|
||||||
A trigger_time of zero disables the alarm_trigger service.
|
A trigger_time of zero disables the alarm_trigger service.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, hass, name, code, code_template, disarm_after_trigger,
|
def __init__(
|
||||||
state_topic, command_topic, qos, payload_disarm,
|
self,
|
||||||
payload_arm_home, payload_arm_away, payload_arm_night,
|
hass,
|
||||||
config):
|
name,
|
||||||
|
code,
|
||||||
|
code_template,
|
||||||
|
disarm_after_trigger,
|
||||||
|
state_topic,
|
||||||
|
command_topic,
|
||||||
|
qos,
|
||||||
|
payload_disarm,
|
||||||
|
payload_arm_home,
|
||||||
|
payload_arm_away,
|
||||||
|
payload_arm_night,
|
||||||
|
config,
|
||||||
|
):
|
||||||
"""Init the manual MQTT alarm panel."""
|
"""Init the manual MQTT alarm panel."""
|
||||||
self._state = STATE_ALARM_DISARMED
|
self._state = STATE_ALARM_DISARMED
|
||||||
self._hass = hass
|
self._hass = hass
|
||||||
|
|
@ -172,13 +230,16 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
|
||||||
|
|
||||||
self._delay_time_by_state = {
|
self._delay_time_by_state = {
|
||||||
state: config[state][CONF_DELAY_TIME]
|
state: config[state][CONF_DELAY_TIME]
|
||||||
for state in SUPPORTED_PRETRIGGER_STATES}
|
for state in SUPPORTED_PRETRIGGER_STATES
|
||||||
|
}
|
||||||
self._trigger_time_by_state = {
|
self._trigger_time_by_state = {
|
||||||
state: config[state][CONF_TRIGGER_TIME]
|
state: config[state][CONF_TRIGGER_TIME]
|
||||||
for state in SUPPORTED_PRETRIGGER_STATES}
|
for state in SUPPORTED_PRETRIGGER_STATES
|
||||||
|
}
|
||||||
self._pending_time_by_state = {
|
self._pending_time_by_state = {
|
||||||
state: config[state][CONF_PENDING_TIME]
|
state: config[state][CONF_PENDING_TIME]
|
||||||
for state in SUPPORTED_PENDING_STATES}
|
for state in SUPPORTED_PENDING_STATES
|
||||||
|
}
|
||||||
|
|
||||||
self._state_topic = state_topic
|
self._state_topic = state_topic
|
||||||
self._command_topic = command_topic
|
self._command_topic = command_topic
|
||||||
|
|
@ -205,15 +266,17 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
|
||||||
if self._within_pending_time(self._state):
|
if self._within_pending_time(self._state):
|
||||||
return STATE_ALARM_PENDING
|
return STATE_ALARM_PENDING
|
||||||
trigger_time = self._trigger_time_by_state[self._previous_state]
|
trigger_time = self._trigger_time_by_state[self._previous_state]
|
||||||
if (self._state_ts + self._pending_time(self._state) +
|
if (
|
||||||
trigger_time) < dt_util.utcnow():
|
self._state_ts + self._pending_time(self._state) + trigger_time
|
||||||
|
) < dt_util.utcnow():
|
||||||
if self._disarm_after_trigger:
|
if self._disarm_after_trigger:
|
||||||
return STATE_ALARM_DISARMED
|
return STATE_ALARM_DISARMED
|
||||||
self._state = self._previous_state
|
self._state = self._previous_state
|
||||||
return self._state
|
return self._state
|
||||||
|
|
||||||
if self._state in SUPPORTED_PENDING_STATES and \
|
if self._state in SUPPORTED_PENDING_STATES and self._within_pending_time(
|
||||||
self._within_pending_time(self._state):
|
self._state
|
||||||
|
):
|
||||||
return STATE_ALARM_PENDING
|
return STATE_ALARM_PENDING
|
||||||
|
|
||||||
return self._state
|
return self._state
|
||||||
|
|
@ -241,9 +304,9 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
|
||||||
"""Return one or more digits/characters."""
|
"""Return one or more digits/characters."""
|
||||||
if self._code is None:
|
if self._code is None:
|
||||||
return None
|
return None
|
||||||
if isinstance(self._code, str) and re.search('^\\d+$', self._code):
|
if isinstance(self._code, str) and re.search("^\\d+$", self._code):
|
||||||
return 'Number'
|
return "Number"
|
||||||
return 'Any'
|
return "Any"
|
||||||
|
|
||||||
def alarm_disarm(self, code=None):
|
def alarm_disarm(self, code=None):
|
||||||
"""Send disarm command."""
|
"""Send disarm command."""
|
||||||
|
|
@ -299,17 +362,19 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
|
||||||
pending_time = self._pending_time(state)
|
pending_time = self._pending_time(state)
|
||||||
if state == STATE_ALARM_TRIGGERED:
|
if state == STATE_ALARM_TRIGGERED:
|
||||||
track_point_in_time(
|
track_point_in_time(
|
||||||
self._hass, self.async_update_ha_state,
|
self._hass, self.async_update_ha_state, self._state_ts + pending_time
|
||||||
self._state_ts + pending_time)
|
)
|
||||||
|
|
||||||
trigger_time = self._trigger_time_by_state[self._previous_state]
|
trigger_time = self._trigger_time_by_state[self._previous_state]
|
||||||
track_point_in_time(
|
track_point_in_time(
|
||||||
self._hass, self.async_update_ha_state,
|
self._hass,
|
||||||
self._state_ts + pending_time + trigger_time)
|
self.async_update_ha_state,
|
||||||
|
self._state_ts + pending_time + trigger_time,
|
||||||
|
)
|
||||||
elif state in SUPPORTED_PENDING_STATES and pending_time:
|
elif state in SUPPORTED_PENDING_STATES and pending_time:
|
||||||
track_point_in_time(
|
track_point_in_time(
|
||||||
self._hass, self.async_update_ha_state,
|
self._hass, self.async_update_ha_state, self._state_ts + pending_time
|
||||||
self._state_ts + pending_time)
|
)
|
||||||
|
|
||||||
def _validate_code(self, code, state):
|
def _validate_code(self, code, state):
|
||||||
"""Validate given code."""
|
"""Validate given code."""
|
||||||
|
|
@ -318,8 +383,7 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
|
||||||
if isinstance(self._code, str):
|
if isinstance(self._code, str):
|
||||||
alarm_code = self._code
|
alarm_code = self._code
|
||||||
else:
|
else:
|
||||||
alarm_code = self._code.render(from_state=self._state,
|
alarm_code = self._code.render(from_state=self._state, to_state=state)
|
||||||
to_state=state)
|
|
||||||
check = not alarm_code or code == alarm_code
|
check = not alarm_code or code == alarm_code
|
||||||
if not check:
|
if not check:
|
||||||
_LOGGER.warning("Invalid code given for %s", state)
|
_LOGGER.warning("Invalid code given for %s", state)
|
||||||
|
|
@ -361,10 +425,12 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
|
||||||
return
|
return
|
||||||
|
|
||||||
return mqtt.async_subscribe(
|
return mqtt.async_subscribe(
|
||||||
self.hass, self._command_topic, message_received, self._qos)
|
self.hass, self._command_topic, message_received, self._qos
|
||||||
|
)
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def _async_state_changed_listener(self, entity_id, old_state, new_state):
|
def _async_state_changed_listener(self, entity_id, old_state, new_state):
|
||||||
"""Publish state change to MQTT."""
|
"""Publish state change to MQTT."""
|
||||||
mqtt.async_publish(
|
mqtt.async_publish(
|
||||||
self.hass, self._state_topic, new_state.state, self._qos, True)
|
self.hass, self._state_topic, new_state.state, self._qos, True
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -14,69 +14,100 @@ from homeassistant.core import callback
|
||||||
import homeassistant.components.alarm_control_panel as alarm
|
import homeassistant.components.alarm_control_panel as alarm
|
||||||
from homeassistant.components import mqtt
|
from homeassistant.components import mqtt
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
|
STATE_ALARM_ARMED_AWAY,
|
||||||
STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, STATE_UNKNOWN,
|
STATE_ALARM_ARMED_HOME,
|
||||||
CONF_NAME, CONF_CODE)
|
STATE_ALARM_DISARMED,
|
||||||
|
STATE_ALARM_PENDING,
|
||||||
|
STATE_ALARM_TRIGGERED,
|
||||||
|
STATE_UNKNOWN,
|
||||||
|
CONF_NAME,
|
||||||
|
CONF_CODE,
|
||||||
|
)
|
||||||
from homeassistant.components.mqtt import (
|
from homeassistant.components.mqtt import (
|
||||||
CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_COMMAND_TOPIC,
|
CONF_AVAILABILITY_TOPIC,
|
||||||
CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS,
|
CONF_STATE_TOPIC,
|
||||||
CONF_RETAIN, MqttAvailability)
|
CONF_COMMAND_TOPIC,
|
||||||
|
CONF_PAYLOAD_AVAILABLE,
|
||||||
|
CONF_PAYLOAD_NOT_AVAILABLE,
|
||||||
|
CONF_QOS,
|
||||||
|
CONF_RETAIN,
|
||||||
|
MqttAvailability,
|
||||||
|
)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
CONF_PAYLOAD_DISARM = 'payload_disarm'
|
CONF_PAYLOAD_DISARM = "payload_disarm"
|
||||||
CONF_PAYLOAD_ARM_HOME = 'payload_arm_home'
|
CONF_PAYLOAD_ARM_HOME = "payload_arm_home"
|
||||||
CONF_PAYLOAD_ARM_AWAY = 'payload_arm_away'
|
CONF_PAYLOAD_ARM_AWAY = "payload_arm_away"
|
||||||
|
|
||||||
DEFAULT_ARM_AWAY = 'ARM_AWAY'
|
DEFAULT_ARM_AWAY = "ARM_AWAY"
|
||||||
DEFAULT_ARM_HOME = 'ARM_HOME'
|
DEFAULT_ARM_HOME = "ARM_HOME"
|
||||||
DEFAULT_DISARM = 'DISARM'
|
DEFAULT_DISARM = "DISARM"
|
||||||
DEFAULT_NAME = 'MQTT Alarm'
|
DEFAULT_NAME = "MQTT Alarm"
|
||||||
DEPENDENCIES = ['mqtt']
|
DEPENDENCIES = ["mqtt"]
|
||||||
|
|
||||||
PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend(
|
||||||
vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
{
|
||||||
vol.Required(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||||
vol.Optional(CONF_CODE): cv.string,
|
vol.Required(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
||||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
vol.Optional(CONF_CODE): cv.string,
|
||||||
vol.Optional(CONF_PAYLOAD_ARM_AWAY, default=DEFAULT_ARM_AWAY): cv.string,
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||||
vol.Optional(CONF_PAYLOAD_ARM_HOME, default=DEFAULT_ARM_HOME): cv.string,
|
vol.Optional(CONF_PAYLOAD_ARM_AWAY, default=DEFAULT_ARM_AWAY): cv.string,
|
||||||
vol.Optional(CONF_PAYLOAD_DISARM, default=DEFAULT_DISARM): cv.string,
|
vol.Optional(CONF_PAYLOAD_ARM_HOME, default=DEFAULT_ARM_HOME): cv.string,
|
||||||
}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema)
|
vol.Optional(CONF_PAYLOAD_DISARM, default=DEFAULT_DISARM): cv.string,
|
||||||
|
}
|
||||||
|
).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema)
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_setup_platform(hass, config, async_add_entities,
|
def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||||
discovery_info=None):
|
|
||||||
"""Set up the MQTT Alarm Control Panel platform."""
|
"""Set up the MQTT Alarm Control Panel platform."""
|
||||||
if discovery_info is not None:
|
if discovery_info is not None:
|
||||||
config = PLATFORM_SCHEMA(discovery_info)
|
config = PLATFORM_SCHEMA(discovery_info)
|
||||||
|
|
||||||
async_add_entities([MqttAlarm(
|
async_add_entities(
|
||||||
config.get(CONF_NAME),
|
[
|
||||||
config.get(CONF_STATE_TOPIC),
|
MqttAlarm(
|
||||||
config.get(CONF_COMMAND_TOPIC),
|
config.get(CONF_NAME),
|
||||||
config.get(CONF_QOS),
|
config.get(CONF_STATE_TOPIC),
|
||||||
config.get(CONF_RETAIN),
|
config.get(CONF_COMMAND_TOPIC),
|
||||||
config.get(CONF_PAYLOAD_DISARM),
|
config.get(CONF_QOS),
|
||||||
config.get(CONF_PAYLOAD_ARM_HOME),
|
config.get(CONF_RETAIN),
|
||||||
config.get(CONF_PAYLOAD_ARM_AWAY),
|
config.get(CONF_PAYLOAD_DISARM),
|
||||||
config.get(CONF_CODE),
|
config.get(CONF_PAYLOAD_ARM_HOME),
|
||||||
config.get(CONF_AVAILABILITY_TOPIC),
|
config.get(CONF_PAYLOAD_ARM_AWAY),
|
||||||
config.get(CONF_PAYLOAD_AVAILABLE),
|
config.get(CONF_CODE),
|
||||||
config.get(CONF_PAYLOAD_NOT_AVAILABLE))])
|
config.get(CONF_AVAILABILITY_TOPIC),
|
||||||
|
config.get(CONF_PAYLOAD_AVAILABLE),
|
||||||
|
config.get(CONF_PAYLOAD_NOT_AVAILABLE),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel):
|
class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel):
|
||||||
"""Representation of a MQTT alarm status."""
|
"""Representation of a MQTT alarm status."""
|
||||||
|
|
||||||
def __init__(self, name, state_topic, command_topic, qos, retain,
|
def __init__(
|
||||||
payload_disarm, payload_arm_home, payload_arm_away, code,
|
self,
|
||||||
availability_topic, payload_available, payload_not_available):
|
name,
|
||||||
|
state_topic,
|
||||||
|
command_topic,
|
||||||
|
qos,
|
||||||
|
retain,
|
||||||
|
payload_disarm,
|
||||||
|
payload_arm_home,
|
||||||
|
payload_arm_away,
|
||||||
|
code,
|
||||||
|
availability_topic,
|
||||||
|
payload_available,
|
||||||
|
payload_not_available,
|
||||||
|
):
|
||||||
"""Init the MQTT Alarm Control Panel."""
|
"""Init the MQTT Alarm Control Panel."""
|
||||||
super().__init__(availability_topic, qos, payload_available,
|
super().__init__(
|
||||||
payload_not_available)
|
availability_topic, qos, payload_available, payload_not_available
|
||||||
|
)
|
||||||
self._state = STATE_UNKNOWN
|
self._state = STATE_UNKNOWN
|
||||||
self._name = name
|
self._name = name
|
||||||
self._state_topic = state_topic
|
self._state_topic = state_topic
|
||||||
|
|
@ -96,16 +127,21 @@ class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel):
|
||||||
@callback
|
@callback
|
||||||
def message_received(topic, payload, qos):
|
def message_received(topic, payload, qos):
|
||||||
"""Run when new MQTT message has been received."""
|
"""Run when new MQTT message has been received."""
|
||||||
if payload not in (STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME,
|
if payload not in (
|
||||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_PENDING,
|
STATE_ALARM_DISARMED,
|
||||||
STATE_ALARM_TRIGGERED):
|
STATE_ALARM_ARMED_HOME,
|
||||||
|
STATE_ALARM_ARMED_AWAY,
|
||||||
|
STATE_ALARM_PENDING,
|
||||||
|
STATE_ALARM_TRIGGERED,
|
||||||
|
):
|
||||||
_LOGGER.warning("Received unexpected payload: %s", payload)
|
_LOGGER.warning("Received unexpected payload: %s", payload)
|
||||||
return
|
return
|
||||||
self._state = payload
|
self._state = payload
|
||||||
self.async_schedule_update_ha_state()
|
self.async_schedule_update_ha_state()
|
||||||
|
|
||||||
yield from mqtt.async_subscribe(
|
yield from mqtt.async_subscribe(
|
||||||
self.hass, self._state_topic, message_received, self._qos)
|
self.hass, self._state_topic, message_received, self._qos
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def should_poll(self):
|
def should_poll(self):
|
||||||
|
|
@ -127,9 +163,9 @@ class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel):
|
||||||
"""Return one or more digits/characters."""
|
"""Return one or more digits/characters."""
|
||||||
if self._code is None:
|
if self._code is None:
|
||||||
return None
|
return None
|
||||||
if isinstance(self._code, str) and re.search('^\\d+$', self._code):
|
if isinstance(self._code, str) and re.search("^\\d+$", self._code):
|
||||||
return 'Number'
|
return "Number"
|
||||||
return 'Any'
|
return "Any"
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_alarm_disarm(self, code=None):
|
def async_alarm_disarm(self, code=None):
|
||||||
|
|
@ -137,11 +173,15 @@ class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel):
|
||||||
|
|
||||||
This method is a coroutine.
|
This method is a coroutine.
|
||||||
"""
|
"""
|
||||||
if not self._validate_code(code, 'disarming'):
|
if not self._validate_code(code, "disarming"):
|
||||||
return
|
return
|
||||||
mqtt.async_publish(
|
mqtt.async_publish(
|
||||||
self.hass, self._command_topic, self._payload_disarm, self._qos,
|
self.hass,
|
||||||
self._retain)
|
self._command_topic,
|
||||||
|
self._payload_disarm,
|
||||||
|
self._qos,
|
||||||
|
self._retain,
|
||||||
|
)
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_alarm_arm_home(self, code=None):
|
def async_alarm_arm_home(self, code=None):
|
||||||
|
|
@ -149,11 +189,15 @@ class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel):
|
||||||
|
|
||||||
This method is a coroutine.
|
This method is a coroutine.
|
||||||
"""
|
"""
|
||||||
if not self._validate_code(code, 'arming home'):
|
if not self._validate_code(code, "arming home"):
|
||||||
return
|
return
|
||||||
mqtt.async_publish(
|
mqtt.async_publish(
|
||||||
self.hass, self._command_topic, self._payload_arm_home, self._qos,
|
self.hass,
|
||||||
self._retain)
|
self._command_topic,
|
||||||
|
self._payload_arm_home,
|
||||||
|
self._qos,
|
||||||
|
self._retain,
|
||||||
|
)
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_alarm_arm_away(self, code=None):
|
def async_alarm_arm_away(self, code=None):
|
||||||
|
|
@ -161,15 +205,19 @@ class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel):
|
||||||
|
|
||||||
This method is a coroutine.
|
This method is a coroutine.
|
||||||
"""
|
"""
|
||||||
if not self._validate_code(code, 'arming away'):
|
if not self._validate_code(code, "arming away"):
|
||||||
return
|
return
|
||||||
mqtt.async_publish(
|
mqtt.async_publish(
|
||||||
self.hass, self._command_topic, self._payload_arm_away, self._qos,
|
self.hass,
|
||||||
self._retain)
|
self._command_topic,
|
||||||
|
self._payload_arm_away,
|
||||||
|
self._qos,
|
||||||
|
self._retain,
|
||||||
|
)
|
||||||
|
|
||||||
def _validate_code(self, code, state):
|
def _validate_code(self, code, state):
|
||||||
"""Validate given code."""
|
"""Validate given code."""
|
||||||
check = self._code is None or code == self._code
|
check = self._code is None or code == self._code
|
||||||
if not check:
|
if not check:
|
||||||
_LOGGER.warning('Wrong code entered for %s', state)
|
_LOGGER.warning("Wrong code entered for %s", state)
|
||||||
return check
|
return check
|
||||||
|
|
|
||||||
|
|
@ -12,23 +12,31 @@ import voluptuous as vol
|
||||||
import homeassistant.components.alarm_control_panel as alarm
|
import homeassistant.components.alarm_control_panel as alarm
|
||||||
from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
|
from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_HOST, CONF_NAME, CONF_PORT, STATE_ALARM_ARMED_AWAY,
|
CONF_HOST,
|
||||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_UNKNOWN)
|
CONF_NAME,
|
||||||
|
CONF_PORT,
|
||||||
|
STATE_ALARM_ARMED_AWAY,
|
||||||
|
STATE_ALARM_ARMED_HOME,
|
||||||
|
STATE_ALARM_DISARMED,
|
||||||
|
STATE_UNKNOWN,
|
||||||
|
)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
REQUIREMENTS = ['pynx584==0.4']
|
REQUIREMENTS = ["pynx584==0.4"]
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DEFAULT_HOST = 'localhost'
|
DEFAULT_HOST = "localhost"
|
||||||
DEFAULT_NAME = 'NX584'
|
DEFAULT_NAME = "NX584"
|
||||||
DEFAULT_PORT = 5007
|
DEFAULT_PORT = 5007
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||||
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
|
{
|
||||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
|
||||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||||
})
|
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
|
|
@ -37,7 +45,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
host = config.get(CONF_HOST)
|
host = config.get(CONF_HOST)
|
||||||
port = config.get(CONF_PORT)
|
port = config.get(CONF_PORT)
|
||||||
|
|
||||||
url = 'http://{}:{}'.format(host, port)
|
url = "http://{}:{}".format(host, port)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
add_entities([NX584Alarm(hass, url, name)])
|
add_entities([NX584Alarm(hass, url, name)])
|
||||||
|
|
@ -52,6 +60,7 @@ class NX584Alarm(alarm.AlarmControlPanel):
|
||||||
def __init__(self, hass, url, name):
|
def __init__(self, hass, url, name):
|
||||||
"""Init the nx584 alarm panel."""
|
"""Init the nx584 alarm panel."""
|
||||||
from nx584 import client
|
from nx584 import client
|
||||||
|
|
||||||
self._hass = hass
|
self._hass = hass
|
||||||
self._name = name
|
self._name = name
|
||||||
self._url = url
|
self._url = url
|
||||||
|
|
@ -70,7 +79,7 @@ class NX584Alarm(alarm.AlarmControlPanel):
|
||||||
@property
|
@property
|
||||||
def code_format(self):
|
def code_format(self):
|
||||||
"""Return one or more digits/characters."""
|
"""Return one or more digits/characters."""
|
||||||
return 'Number'
|
return "Number"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self):
|
def state(self):
|
||||||
|
|
@ -83,8 +92,10 @@ class NX584Alarm(alarm.AlarmControlPanel):
|
||||||
part = self._alarm.list_partitions()[0]
|
part = self._alarm.list_partitions()[0]
|
||||||
zones = self._alarm.list_zones()
|
zones = self._alarm.list_zones()
|
||||||
except requests.exceptions.ConnectionError as ex:
|
except requests.exceptions.ConnectionError as ex:
|
||||||
_LOGGER.error("Unable to connect to %(host)s: %(reason)s",
|
_LOGGER.error(
|
||||||
dict(host=self._url, reason=ex))
|
"Unable to connect to %(host)s: %(reason)s",
|
||||||
|
dict(host=self._url, reason=ex),
|
||||||
|
)
|
||||||
self._state = STATE_UNKNOWN
|
self._state = STATE_UNKNOWN
|
||||||
zones = []
|
zones = []
|
||||||
except IndexError:
|
except IndexError:
|
||||||
|
|
@ -94,13 +105,15 @@ class NX584Alarm(alarm.AlarmControlPanel):
|
||||||
|
|
||||||
bypassed = False
|
bypassed = False
|
||||||
for zone in zones:
|
for zone in zones:
|
||||||
if zone['bypassed']:
|
if zone["bypassed"]:
|
||||||
_LOGGER.debug("Zone %(zone)s is bypassed, assuming HOME",
|
_LOGGER.debug(
|
||||||
dict(zone=zone['number']))
|
"Zone %(zone)s is bypassed, assuming HOME",
|
||||||
|
dict(zone=zone["number"]),
|
||||||
|
)
|
||||||
bypassed = True
|
bypassed = True
|
||||||
break
|
break
|
||||||
|
|
||||||
if not part['armed']:
|
if not part["armed"]:
|
||||||
self._state = STATE_ALARM_DISARMED
|
self._state = STATE_ALARM_DISARMED
|
||||||
elif bypassed:
|
elif bypassed:
|
||||||
self._state = STATE_ALARM_ARMED_HOME
|
self._state = STATE_ALARM_ARMED_HOME
|
||||||
|
|
@ -113,8 +126,8 @@ class NX584Alarm(alarm.AlarmControlPanel):
|
||||||
|
|
||||||
def alarm_arm_home(self, code=None):
|
def alarm_arm_home(self, code=None):
|
||||||
"""Send arm home command."""
|
"""Send arm home command."""
|
||||||
self._alarm.arm('stay')
|
self._alarm.arm("stay")
|
||||||
|
|
||||||
def alarm_arm_away(self, code=None):
|
def alarm_arm_away(self, code=None):
|
||||||
"""Send arm away command."""
|
"""Send arm away command."""
|
||||||
self._alarm.arm('exit')
|
self._alarm.arm("exit")
|
||||||
|
|
|
||||||
|
|
@ -9,24 +9,27 @@ import logging
|
||||||
|
|
||||||
import homeassistant.components.alarm_control_panel as alarm
|
import homeassistant.components.alarm_control_panel as alarm
|
||||||
from homeassistant.components.satel_integra import (
|
from homeassistant.components.satel_integra import (
|
||||||
CONF_ARM_HOME_MODE, DATA_SATEL, SIGNAL_PANEL_MESSAGE)
|
CONF_ARM_HOME_MODE,
|
||||||
|
DATA_SATEL,
|
||||||
|
SIGNAL_PANEL_MESSAGE,
|
||||||
|
)
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DEPENDENCIES = ['satel_integra']
|
DEPENDENCIES = ["satel_integra"]
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_setup_platform(hass, config, async_add_entities,
|
def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||||
discovery_info=None):
|
|
||||||
"""Set up for Satel Integra alarm panels."""
|
"""Set up for Satel Integra alarm panels."""
|
||||||
if not discovery_info:
|
if not discovery_info:
|
||||||
return
|
return
|
||||||
|
|
||||||
device = SatelIntegraAlarmPanel(
|
device = SatelIntegraAlarmPanel(
|
||||||
"Alarm Panel", discovery_info.get(CONF_ARM_HOME_MODE))
|
"Alarm Panel", discovery_info.get(CONF_ARM_HOME_MODE)
|
||||||
|
)
|
||||||
async_add_entities([device])
|
async_add_entities([device])
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -43,7 +46,8 @@ class SatelIntegraAlarmPanel(alarm.AlarmControlPanel):
|
||||||
def async_added_to_hass(self):
|
def async_added_to_hass(self):
|
||||||
"""Register callbacks."""
|
"""Register callbacks."""
|
||||||
async_dispatcher_connect(
|
async_dispatcher_connect(
|
||||||
self.hass, SIGNAL_PANEL_MESSAGE, self._message_callback)
|
self.hass, SIGNAL_PANEL_MESSAGE, self._message_callback
|
||||||
|
)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _message_callback(self, message):
|
def _message_callback(self, message):
|
||||||
|
|
@ -67,7 +71,7 @@ class SatelIntegraAlarmPanel(alarm.AlarmControlPanel):
|
||||||
@property
|
@property
|
||||||
def code_format(self):
|
def code_format(self):
|
||||||
"""Return the regex for code format or None if no code is required."""
|
"""Return the regex for code format or None if no code is required."""
|
||||||
return 'Number'
|
return "Number"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self):
|
def state(self):
|
||||||
|
|
@ -90,5 +94,4 @@ class SatelIntegraAlarmPanel(alarm.AlarmControlPanel):
|
||||||
def async_alarm_arm_home(self, code=None):
|
def async_alarm_arm_home(self, code=None):
|
||||||
"""Send arm home command."""
|
"""Send arm home command."""
|
||||||
if code:
|
if code:
|
||||||
yield from self.hass.data[DATA_SATEL].arm(
|
yield from self.hass.data[DATA_SATEL].arm(code, self._arm_home_mode)
|
||||||
code, self._arm_home_mode)
|
|
||||||
|
|
|
||||||
|
|
@ -10,33 +10,44 @@ import re
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.alarm_control_panel import (
|
from homeassistant.components.alarm_control_panel import (
|
||||||
PLATFORM_SCHEMA, AlarmControlPanel)
|
PLATFORM_SCHEMA,
|
||||||
|
AlarmControlPanel,
|
||||||
|
)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_CODE, CONF_NAME, CONF_PASSWORD, CONF_USERNAME,
|
CONF_CODE,
|
||||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
|
CONF_NAME,
|
||||||
STATE_ALARM_DISARMED, STATE_UNKNOWN)
|
CONF_PASSWORD,
|
||||||
|
CONF_USERNAME,
|
||||||
|
STATE_ALARM_ARMED_AWAY,
|
||||||
|
STATE_ALARM_ARMED_HOME,
|
||||||
|
STATE_ALARM_DISARMED,
|
||||||
|
STATE_UNKNOWN,
|
||||||
|
)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
REQUIREMENTS = ['simplisafe-python==2.0.2']
|
REQUIREMENTS = ["simplisafe-python==2.0.2"]
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DEFAULT_NAME = 'SimpliSafe'
|
DEFAULT_NAME = "SimpliSafe"
|
||||||
|
|
||||||
ATTR_ALARM_ACTIVE = "alarm_active"
|
ATTR_ALARM_ACTIVE = "alarm_active"
|
||||||
ATTR_TEMPERATURE = "temperature"
|
ATTR_TEMPERATURE = "temperature"
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||||
vol.Required(CONF_PASSWORD): cv.string,
|
{
|
||||||
vol.Required(CONF_USERNAME): cv.string,
|
vol.Required(CONF_PASSWORD): cv.string,
|
||||||
vol.Optional(CONF_CODE): cv.string,
|
vol.Required(CONF_USERNAME): cv.string,
|
||||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
vol.Optional(CONF_CODE): cv.string,
|
||||||
})
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
"""Set up the SimpliSafe platform."""
|
"""Set up the SimpliSafe platform."""
|
||||||
from simplipy.api import SimpliSafeApiInterface, SimpliSafeAPIException
|
from simplipy.api import SimpliSafeApiInterface, SimpliSafeAPIException
|
||||||
|
|
||||||
name = config.get(CONF_NAME)
|
name = config.get(CONF_NAME)
|
||||||
code = config.get(CONF_CODE)
|
code = config.get(CONF_CODE)
|
||||||
username = config.get(CONF_USERNAME)
|
username = config.get(CONF_USERNAME)
|
||||||
|
|
@ -75,27 +86,30 @@ class SimpliSafeAlarm(AlarmControlPanel):
|
||||||
"""Return the name of the device."""
|
"""Return the name of the device."""
|
||||||
if self._name is not None:
|
if self._name is not None:
|
||||||
return self._name
|
return self._name
|
||||||
return 'Alarm {}'.format(self.simplisafe.location_id)
|
return "Alarm {}".format(self.simplisafe.location_id)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def code_format(self):
|
def code_format(self):
|
||||||
"""Return one or more digits/characters."""
|
"""Return one or more digits/characters."""
|
||||||
if self._code is None:
|
if self._code is None:
|
||||||
return None
|
return None
|
||||||
if isinstance(self._code, str) and re.search('^\\d+$', self._code):
|
if isinstance(self._code, str) and re.search("^\\d+$", self._code):
|
||||||
return 'Number'
|
return "Number"
|
||||||
return 'Any'
|
return "Any"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self):
|
def state(self):
|
||||||
"""Return the state of the device."""
|
"""Return the state of the device."""
|
||||||
status = self.simplisafe.state
|
status = self.simplisafe.state
|
||||||
if status.lower() == 'off':
|
if status.lower() == "off":
|
||||||
state = STATE_ALARM_DISARMED
|
state = STATE_ALARM_DISARMED
|
||||||
elif status.lower() == 'home' or status.lower() == 'home_count':
|
elif status.lower() == "home" or status.lower() == "home_count":
|
||||||
state = STATE_ALARM_ARMED_HOME
|
state = STATE_ALARM_ARMED_HOME
|
||||||
elif (status.lower() == 'away' or status.lower() == 'exitDelay' or
|
elif (
|
||||||
status.lower() == 'away_count'):
|
status.lower() == "away"
|
||||||
|
or status.lower() == "exitDelay"
|
||||||
|
or status.lower() == "away_count"
|
||||||
|
):
|
||||||
state = STATE_ALARM_ARMED_AWAY
|
state = STATE_ALARM_ARMED_AWAY
|
||||||
else:
|
else:
|
||||||
state = STATE_UNKNOWN
|
state = STATE_UNKNOWN
|
||||||
|
|
@ -118,23 +132,23 @@ class SimpliSafeAlarm(AlarmControlPanel):
|
||||||
|
|
||||||
def alarm_disarm(self, code=None):
|
def alarm_disarm(self, code=None):
|
||||||
"""Send disarm command."""
|
"""Send disarm command."""
|
||||||
if not self._validate_code(code, 'disarming'):
|
if not self._validate_code(code, "disarming"):
|
||||||
return
|
return
|
||||||
self.simplisafe.set_state('off')
|
self.simplisafe.set_state("off")
|
||||||
_LOGGER.info("SimpliSafe alarm disarming")
|
_LOGGER.info("SimpliSafe alarm disarming")
|
||||||
|
|
||||||
def alarm_arm_home(self, code=None):
|
def alarm_arm_home(self, code=None):
|
||||||
"""Send arm home command."""
|
"""Send arm home command."""
|
||||||
if not self._validate_code(code, 'arming home'):
|
if not self._validate_code(code, "arming home"):
|
||||||
return
|
return
|
||||||
self.simplisafe.set_state('home')
|
self.simplisafe.set_state("home")
|
||||||
_LOGGER.info("SimpliSafe alarm arming home")
|
_LOGGER.info("SimpliSafe alarm arming home")
|
||||||
|
|
||||||
def alarm_arm_away(self, code=None):
|
def alarm_arm_away(self, code=None):
|
||||||
"""Send arm away command."""
|
"""Send arm away command."""
|
||||||
if not self._validate_code(code, 'arming away'):
|
if not self._validate_code(code, "arming away"):
|
||||||
return
|
return
|
||||||
self.simplisafe.set_state('away')
|
self.simplisafe.set_state("away")
|
||||||
_LOGGER.info("SimpliSafe alarm arming away")
|
_LOGGER.info("SimpliSafe alarm arming away")
|
||||||
|
|
||||||
def _validate_code(self, code, state):
|
def _validate_code(self, code, state):
|
||||||
|
|
|
||||||
|
|
@ -9,17 +9,24 @@ import logging
|
||||||
|
|
||||||
import homeassistant.components.alarm_control_panel as alarm
|
import homeassistant.components.alarm_control_panel as alarm
|
||||||
from homeassistant.components.spc import (
|
from homeassistant.components.spc import (
|
||||||
ATTR_DISCOVER_AREAS, DATA_API, DATA_REGISTRY, SpcWebGateway)
|
ATTR_DISCOVER_AREAS,
|
||||||
|
DATA_API,
|
||||||
|
DATA_REGISTRY,
|
||||||
|
SpcWebGateway,
|
||||||
|
)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
|
STATE_ALARM_ARMED_AWAY,
|
||||||
STATE_UNKNOWN)
|
STATE_ALARM_ARMED_HOME,
|
||||||
|
STATE_ALARM_DISARMED,
|
||||||
|
STATE_UNKNOWN,
|
||||||
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
SPC_AREA_MODE_TO_STATE = {
|
SPC_AREA_MODE_TO_STATE = {
|
||||||
'0': STATE_ALARM_DISARMED,
|
"0": STATE_ALARM_DISARMED,
|
||||||
'1': STATE_ALARM_ARMED_HOME,
|
"1": STATE_ALARM_ARMED_HOME,
|
||||||
'3': STATE_ALARM_ARMED_AWAY,
|
"3": STATE_ALARM_ARMED_AWAY,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -29,16 +36,13 @@ def _get_alarm_state(spc_mode):
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_setup_platform(hass, config, async_add_entities,
|
def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||||
discovery_info=None):
|
|
||||||
"""Set up the SPC alarm control panel platform."""
|
"""Set up the SPC alarm control panel platform."""
|
||||||
if (discovery_info is None or
|
if discovery_info is None or discovery_info[ATTR_DISCOVER_AREAS] is None:
|
||||||
discovery_info[ATTR_DISCOVER_AREAS] is None):
|
|
||||||
return
|
return
|
||||||
|
|
||||||
api = hass.data[DATA_API]
|
api = hass.data[DATA_API]
|
||||||
devices = [SpcAlarm(api, area)
|
devices = [SpcAlarm(api, area) for area in discovery_info[ATTR_DISCOVER_AREAS]]
|
||||||
for area in discovery_info[ATTR_DISCOVER_AREAS]]
|
|
||||||
|
|
||||||
async_add_entities(devices)
|
async_add_entities(devices)
|
||||||
|
|
||||||
|
|
@ -48,26 +52,25 @@ class SpcAlarm(alarm.AlarmControlPanel):
|
||||||
|
|
||||||
def __init__(self, api, area):
|
def __init__(self, api, area):
|
||||||
"""Initialize the SPC alarm panel."""
|
"""Initialize the SPC alarm panel."""
|
||||||
self._area_id = area['id']
|
self._area_id = area["id"]
|
||||||
self._name = area['name']
|
self._name = area["name"]
|
||||||
self._state = _get_alarm_state(area['mode'])
|
self._state = _get_alarm_state(area["mode"])
|
||||||
if self._state == STATE_ALARM_DISARMED:
|
if self._state == STATE_ALARM_DISARMED:
|
||||||
self._changed_by = area.get('last_unset_user_name', 'unknown')
|
self._changed_by = area.get("last_unset_user_name", "unknown")
|
||||||
else:
|
else:
|
||||||
self._changed_by = area.get('last_set_user_name', 'unknown')
|
self._changed_by = area.get("last_set_user_name", "unknown")
|
||||||
self._api = api
|
self._api = api
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_added_to_hass(self):
|
def async_added_to_hass(self):
|
||||||
"""Call for adding new entities."""
|
"""Call for adding new entities."""
|
||||||
self.hass.data[DATA_REGISTRY].register_alarm_device(
|
self.hass.data[DATA_REGISTRY].register_alarm_device(self._area_id, self)
|
||||||
self._area_id, self)
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_update_from_spc(self, state, extra):
|
def async_update_from_spc(self, state, extra):
|
||||||
"""Update the alarm panel with a new state."""
|
"""Update the alarm panel with a new state."""
|
||||||
self._state = state
|
self._state = state
|
||||||
self._changed_by = extra.get('changed_by', 'unknown')
|
self._changed_by = extra.get("changed_by", "unknown")
|
||||||
self.async_schedule_update_ha_state()
|
self.async_schedule_update_ha_state()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
@ -94,16 +97,19 @@ class SpcAlarm(alarm.AlarmControlPanel):
|
||||||
def async_alarm_disarm(self, code=None):
|
def async_alarm_disarm(self, code=None):
|
||||||
"""Send disarm command."""
|
"""Send disarm command."""
|
||||||
yield from self._api.send_area_command(
|
yield from self._api.send_area_command(
|
||||||
self._area_id, SpcWebGateway.AREA_COMMAND_UNSET)
|
self._area_id, SpcWebGateway.AREA_COMMAND_UNSET
|
||||||
|
)
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_alarm_arm_home(self, code=None):
|
def async_alarm_arm_home(self, code=None):
|
||||||
"""Send arm home command."""
|
"""Send arm home command."""
|
||||||
yield from self._api.send_area_command(
|
yield from self._api.send_area_command(
|
||||||
self._area_id, SpcWebGateway.AREA_COMMAND_PART_SET)
|
self._area_id, SpcWebGateway.AREA_COMMAND_PART_SET
|
||||||
|
)
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_alarm_arm_away(self, code=None):
|
def async_alarm_arm_away(self, code=None):
|
||||||
"""Send arm away command."""
|
"""Send arm away command."""
|
||||||
yield from self._api.send_area_command(
|
yield from self._api.send_area_command(
|
||||||
self._area_id, SpcWebGateway.AREA_COMMAND_SET)
|
self._area_id, SpcWebGateway.AREA_COMMAND_SET
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -12,23 +12,33 @@ import homeassistant.helpers.config_validation as cv
|
||||||
import homeassistant.components.alarm_control_panel as alarm
|
import homeassistant.components.alarm_control_panel as alarm
|
||||||
from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
|
from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_PASSWORD, CONF_USERNAME, STATE_ALARM_ARMED_AWAY,
|
CONF_PASSWORD,
|
||||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED,
|
CONF_USERNAME,
|
||||||
STATE_ALARM_ARMING, STATE_ALARM_DISARMING, STATE_UNKNOWN, CONF_NAME,
|
STATE_ALARM_ARMED_AWAY,
|
||||||
STATE_ALARM_ARMED_CUSTOM_BYPASS)
|
STATE_ALARM_ARMED_HOME,
|
||||||
|
STATE_ALARM_ARMED_NIGHT,
|
||||||
|
STATE_ALARM_DISARMED,
|
||||||
|
STATE_ALARM_ARMING,
|
||||||
|
STATE_ALARM_DISARMING,
|
||||||
|
STATE_UNKNOWN,
|
||||||
|
CONF_NAME,
|
||||||
|
STATE_ALARM_ARMED_CUSTOM_BYPASS,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
REQUIREMENTS = ['total_connect_client==0.18']
|
REQUIREMENTS = ["total_connect_client==0.18"]
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DEFAULT_NAME = 'Total Connect'
|
DEFAULT_NAME = "Total Connect"
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||||
vol.Required(CONF_PASSWORD): cv.string,
|
{
|
||||||
vol.Required(CONF_USERNAME): cv.string,
|
vol.Required(CONF_PASSWORD): cv.string,
|
||||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
vol.Required(CONF_USERNAME): cv.string,
|
||||||
})
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
|
|
@ -53,8 +63,7 @@ class TotalConnect(alarm.AlarmControlPanel):
|
||||||
self._username = username
|
self._username = username
|
||||||
self._password = password
|
self._password = password
|
||||||
self._state = STATE_UNKNOWN
|
self._state = STATE_UNKNOWN
|
||||||
self._client = TotalConnectClient.TotalConnectClient(
|
self._client = TotalConnectClient.TotalConnectClient(username, password)
|
||||||
username, password)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,11 @@ import homeassistant.components.alarm_control_panel as alarm
|
||||||
from homeassistant.components.verisure import CONF_ALARM, CONF_CODE_DIGITS
|
from homeassistant.components.verisure import CONF_ALARM, CONF_CODE_DIGITS
|
||||||
from homeassistant.components.verisure import HUB as hub
|
from homeassistant.components.verisure import HUB as hub
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
|
STATE_ALARM_ARMED_AWAY,
|
||||||
STATE_UNKNOWN)
|
STATE_ALARM_ARMED_HOME,
|
||||||
|
STATE_ALARM_DISARMED,
|
||||||
|
STATE_UNKNOWN,
|
||||||
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -29,10 +32,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
def set_arm_state(state, code=None):
|
def set_arm_state(state, code=None):
|
||||||
"""Send set arm state command."""
|
"""Send set arm state command."""
|
||||||
transaction_id = hub.session.set_arm_state(code, state)[
|
transaction_id = hub.session.set_arm_state(code, state)[
|
||||||
'armStateChangeTransactionId']
|
"armStateChangeTransactionId"
|
||||||
_LOGGER.info('verisure set arm state %s', state)
|
]
|
||||||
|
_LOGGER.info("verisure set arm state %s", state)
|
||||||
transaction = {}
|
transaction = {}
|
||||||
while 'result' not in transaction:
|
while "result" not in transaction:
|
||||||
sleep(0.5)
|
sleep(0.5)
|
||||||
transaction = hub.session.get_arm_state_transaction(transaction_id)
|
transaction = hub.session.get_arm_state_transaction(transaction_id)
|
||||||
# pylint: disable=unexpected-keyword-arg
|
# pylint: disable=unexpected-keyword-arg
|
||||||
|
|
@ -51,7 +55,7 @@ class VerisureAlarm(alarm.AlarmControlPanel):
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Return the name of the device."""
|
"""Return the name of the device."""
|
||||||
return '{} alarm'.format(hub.session.installations[0]['alias'])
|
return "{} alarm".format(hub.session.installations[0]["alias"])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self):
|
def state(self):
|
||||||
|
|
@ -61,7 +65,7 @@ class VerisureAlarm(alarm.AlarmControlPanel):
|
||||||
@property
|
@property
|
||||||
def code_format(self):
|
def code_format(self):
|
||||||
"""Return one or more digits/characters."""
|
"""Return one or more digits/characters."""
|
||||||
return 'Number'
|
return "Number"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def changed_by(self):
|
def changed_by(self):
|
||||||
|
|
@ -72,24 +76,24 @@ class VerisureAlarm(alarm.AlarmControlPanel):
|
||||||
"""Update alarm status."""
|
"""Update alarm status."""
|
||||||
hub.update_overview()
|
hub.update_overview()
|
||||||
status = hub.get_first("$.armState.statusType")
|
status = hub.get_first("$.armState.statusType")
|
||||||
if status == 'DISARMED':
|
if status == "DISARMED":
|
||||||
self._state = STATE_ALARM_DISARMED
|
self._state = STATE_ALARM_DISARMED
|
||||||
elif status == 'ARMED_HOME':
|
elif status == "ARMED_HOME":
|
||||||
self._state = STATE_ALARM_ARMED_HOME
|
self._state = STATE_ALARM_ARMED_HOME
|
||||||
elif status == 'ARMED_AWAY':
|
elif status == "ARMED_AWAY":
|
||||||
self._state = STATE_ALARM_ARMED_AWAY
|
self._state = STATE_ALARM_ARMED_AWAY
|
||||||
elif status != 'PENDING':
|
elif status != "PENDING":
|
||||||
_LOGGER.error('Unknown alarm state %s', status)
|
_LOGGER.error("Unknown alarm state %s", status)
|
||||||
self._changed_by = hub.get_first("$.armState.name")
|
self._changed_by = hub.get_first("$.armState.name")
|
||||||
|
|
||||||
def alarm_disarm(self, code=None):
|
def alarm_disarm(self, code=None):
|
||||||
"""Send disarm command."""
|
"""Send disarm command."""
|
||||||
set_arm_state('DISARMED', code)
|
set_arm_state("DISARMED", code)
|
||||||
|
|
||||||
def alarm_arm_home(self, code=None):
|
def alarm_arm_home(self, code=None):
|
||||||
"""Send arm home command."""
|
"""Send arm home command."""
|
||||||
set_arm_state('ARMED_HOME', code)
|
set_arm_state("ARMED_HOME", code)
|
||||||
|
|
||||||
def alarm_arm_away(self, code=None):
|
def alarm_arm_away(self, code=None):
|
||||||
"""Send arm away command."""
|
"""Send arm away command."""
|
||||||
set_arm_state('ARMED_AWAY', code)
|
set_arm_state("ARMED_AWAY", code)
|
||||||
|
|
|
||||||
|
|
@ -10,14 +10,17 @@ import logging
|
||||||
import homeassistant.components.alarm_control_panel as alarm
|
import homeassistant.components.alarm_control_panel as alarm
|
||||||
from homeassistant.components.wink import DOMAIN, WinkDevice
|
from homeassistant.components.wink import DOMAIN, WinkDevice
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
|
STATE_ALARM_ARMED_AWAY,
|
||||||
STATE_UNKNOWN)
|
STATE_ALARM_ARMED_HOME,
|
||||||
|
STATE_ALARM_DISARMED,
|
||||||
|
STATE_UNKNOWN,
|
||||||
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DEPENDENCIES = ['wink']
|
DEPENDENCIES = ["wink"]
|
||||||
|
|
||||||
STATE_ALARM_PRIVACY = 'Private'
|
STATE_ALARM_PRIVACY = "Private"
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
|
|
@ -31,7 +34,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
camera.capability()
|
camera.capability()
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
_id = camera.object_id() + camera.name()
|
_id = camera.object_id() + camera.name()
|
||||||
if _id not in hass.data[DOMAIN]['unique_ids']:
|
if _id not in hass.data[DOMAIN]["unique_ids"]:
|
||||||
add_entities([WinkCameraDevice(camera, hass)])
|
add_entities([WinkCameraDevice(camera, hass)])
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -41,7 +44,7 @@ class WinkCameraDevice(WinkDevice, alarm.AlarmControlPanel):
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_added_to_hass(self):
|
def async_added_to_hass(self):
|
||||||
"""Call when entity is added to hass."""
|
"""Call when entity is added to hass."""
|
||||||
self.hass.data[DOMAIN]['entities']['alarm_control_panel'].append(self)
|
self.hass.data[DOMAIN]["entities"]["alarm_control_panel"].append(self)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self):
|
def state(self):
|
||||||
|
|
@ -72,6 +75,4 @@ class WinkCameraDevice(WinkDevice, alarm.AlarmControlPanel):
|
||||||
@property
|
@property
|
||||||
def device_state_attributes(self):
|
def device_state_attributes(self):
|
||||||
"""Return the state attributes."""
|
"""Return the state attributes."""
|
||||||
return {
|
return {"private": self.wink.private()}
|
||||||
'private': self.wink.private()
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -9,28 +9,37 @@ import logging
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.alarm_control_panel import (
|
from homeassistant.components.alarm_control_panel import (
|
||||||
AlarmControlPanel, PLATFORM_SCHEMA)
|
AlarmControlPanel,
|
||||||
|
PLATFORM_SCHEMA,
|
||||||
|
)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_PASSWORD, CONF_USERNAME, CONF_NAME,
|
CONF_PASSWORD,
|
||||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED)
|
CONF_USERNAME,
|
||||||
|
CONF_NAME,
|
||||||
|
STATE_ALARM_ARMED_AWAY,
|
||||||
|
STATE_ALARM_ARMED_HOME,
|
||||||
|
STATE_ALARM_DISARMED,
|
||||||
|
)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
REQUIREMENTS = ['yalesmartalarmclient==0.1.4']
|
REQUIREMENTS = ["yalesmartalarmclient==0.1.4"]
|
||||||
|
|
||||||
CONF_AREA_ID = 'area_id'
|
CONF_AREA_ID = "area_id"
|
||||||
|
|
||||||
DEFAULT_NAME = 'Yale Smart Alarm'
|
DEFAULT_NAME = "Yale Smart Alarm"
|
||||||
|
|
||||||
DEFAULT_AREA_ID = '1'
|
DEFAULT_AREA_ID = "1"
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||||
vol.Required(CONF_USERNAME): cv.string,
|
{
|
||||||
vol.Required(CONF_PASSWORD): cv.string,
|
vol.Required(CONF_USERNAME): cv.string,
|
||||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
vol.Required(CONF_PASSWORD): cv.string,
|
||||||
vol.Optional(CONF_AREA_ID, default=DEFAULT_AREA_ID): cv.string,
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||||
})
|
vol.Optional(CONF_AREA_ID, default=DEFAULT_AREA_ID): cv.string,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
|
|
@ -40,8 +49,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
password = config[CONF_PASSWORD]
|
password = config[CONF_PASSWORD]
|
||||||
area_id = config[CONF_AREA_ID]
|
area_id = config[CONF_AREA_ID]
|
||||||
|
|
||||||
from yalesmartalarmclient.client import (
|
from yalesmartalarmclient.client import YaleSmartAlarmClient, AuthenticationError
|
||||||
YaleSmartAlarmClient, AuthenticationError)
|
|
||||||
try:
|
try:
|
||||||
client = YaleSmartAlarmClient(username, password, area_id)
|
client = YaleSmartAlarmClient(username, password, area_id)
|
||||||
except AuthenticationError:
|
except AuthenticationError:
|
||||||
|
|
@ -60,13 +69,16 @@ class YaleAlarmDevice(AlarmControlPanel):
|
||||||
self._client = client
|
self._client = client
|
||||||
self._state = None
|
self._state = None
|
||||||
|
|
||||||
from yalesmartalarmclient.client import (YALE_STATE_DISARM,
|
from yalesmartalarmclient.client import (
|
||||||
YALE_STATE_ARM_PARTIAL,
|
YALE_STATE_DISARM,
|
||||||
YALE_STATE_ARM_FULL)
|
YALE_STATE_ARM_PARTIAL,
|
||||||
|
YALE_STATE_ARM_FULL,
|
||||||
|
)
|
||||||
|
|
||||||
self._state_map = {
|
self._state_map = {
|
||||||
YALE_STATE_DISARM: STATE_ALARM_DISARMED,
|
YALE_STATE_DISARM: STATE_ALARM_DISARMED,
|
||||||
YALE_STATE_ARM_PARTIAL: STATE_ALARM_ARMED_HOME,
|
YALE_STATE_ARM_PARTIAL: STATE_ALARM_ARMED_HOME,
|
||||||
YALE_STATE_ARM_FULL: STATE_ALARM_ARMED_AWAY
|
YALE_STATE_ARM_FULL: STATE_ALARM_ARMED_AWAY,
|
||||||
}
|
}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
|
||||||
|
|
@ -15,87 +15,108 @@ from homeassistant.helpers.discovery import load_platform
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
from homeassistant.components.binary_sensor import DEVICE_CLASSES_SCHEMA
|
from homeassistant.components.binary_sensor import DEVICE_CLASSES_SCHEMA
|
||||||
|
|
||||||
REQUIREMENTS = ['alarmdecoder==1.13.2']
|
REQUIREMENTS = ["alarmdecoder==1.13.2"]
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DOMAIN = 'alarmdecoder'
|
DOMAIN = "alarmdecoder"
|
||||||
|
|
||||||
DATA_AD = 'alarmdecoder'
|
DATA_AD = "alarmdecoder"
|
||||||
|
|
||||||
CONF_DEVICE = 'device'
|
CONF_DEVICE = "device"
|
||||||
CONF_DEVICE_BAUD = 'baudrate'
|
CONF_DEVICE_BAUD = "baudrate"
|
||||||
CONF_DEVICE_HOST = 'host'
|
CONF_DEVICE_HOST = "host"
|
||||||
CONF_DEVICE_PATH = 'path'
|
CONF_DEVICE_PATH = "path"
|
||||||
CONF_DEVICE_PORT = 'port'
|
CONF_DEVICE_PORT = "port"
|
||||||
CONF_DEVICE_TYPE = 'type'
|
CONF_DEVICE_TYPE = "type"
|
||||||
CONF_PANEL_DISPLAY = 'panel_display'
|
CONF_PANEL_DISPLAY = "panel_display"
|
||||||
CONF_ZONE_NAME = 'name'
|
CONF_ZONE_NAME = "name"
|
||||||
CONF_ZONE_TYPE = 'type'
|
CONF_ZONE_TYPE = "type"
|
||||||
CONF_ZONE_RFID = 'rfid'
|
CONF_ZONE_RFID = "rfid"
|
||||||
CONF_ZONES = 'zones'
|
CONF_ZONES = "zones"
|
||||||
CONF_RELAY_ADDR = 'relayaddr'
|
CONF_RELAY_ADDR = "relayaddr"
|
||||||
CONF_RELAY_CHAN = 'relaychan'
|
CONF_RELAY_CHAN = "relaychan"
|
||||||
|
|
||||||
DEFAULT_DEVICE_TYPE = 'socket'
|
DEFAULT_DEVICE_TYPE = "socket"
|
||||||
DEFAULT_DEVICE_HOST = 'localhost'
|
DEFAULT_DEVICE_HOST = "localhost"
|
||||||
DEFAULT_DEVICE_PORT = 10000
|
DEFAULT_DEVICE_PORT = 10000
|
||||||
DEFAULT_DEVICE_PATH = '/dev/ttyUSB0'
|
DEFAULT_DEVICE_PATH = "/dev/ttyUSB0"
|
||||||
DEFAULT_DEVICE_BAUD = 115200
|
DEFAULT_DEVICE_BAUD = 115200
|
||||||
|
|
||||||
DEFAULT_PANEL_DISPLAY = False
|
DEFAULT_PANEL_DISPLAY = False
|
||||||
|
|
||||||
DEFAULT_ZONE_TYPE = 'opening'
|
DEFAULT_ZONE_TYPE = "opening"
|
||||||
|
|
||||||
SIGNAL_PANEL_MESSAGE = 'alarmdecoder.panel_message'
|
SIGNAL_PANEL_MESSAGE = "alarmdecoder.panel_message"
|
||||||
SIGNAL_PANEL_ARM_AWAY = 'alarmdecoder.panel_arm_away'
|
SIGNAL_PANEL_ARM_AWAY = "alarmdecoder.panel_arm_away"
|
||||||
SIGNAL_PANEL_ARM_HOME = 'alarmdecoder.panel_arm_home'
|
SIGNAL_PANEL_ARM_HOME = "alarmdecoder.panel_arm_home"
|
||||||
SIGNAL_PANEL_DISARM = 'alarmdecoder.panel_disarm'
|
SIGNAL_PANEL_DISARM = "alarmdecoder.panel_disarm"
|
||||||
|
|
||||||
SIGNAL_ZONE_FAULT = 'alarmdecoder.zone_fault'
|
SIGNAL_ZONE_FAULT = "alarmdecoder.zone_fault"
|
||||||
SIGNAL_ZONE_RESTORE = 'alarmdecoder.zone_restore'
|
SIGNAL_ZONE_RESTORE = "alarmdecoder.zone_restore"
|
||||||
SIGNAL_RFX_MESSAGE = 'alarmdecoder.rfx_message'
|
SIGNAL_RFX_MESSAGE = "alarmdecoder.rfx_message"
|
||||||
SIGNAL_REL_MESSAGE = 'alarmdecoder.rel_message'
|
SIGNAL_REL_MESSAGE = "alarmdecoder.rel_message"
|
||||||
|
|
||||||
DEVICE_SOCKET_SCHEMA = vol.Schema({
|
DEVICE_SOCKET_SCHEMA = vol.Schema(
|
||||||
vol.Required(CONF_DEVICE_TYPE): 'socket',
|
{
|
||||||
vol.Optional(CONF_DEVICE_HOST, default=DEFAULT_DEVICE_HOST): cv.string,
|
vol.Required(CONF_DEVICE_TYPE): "socket",
|
||||||
vol.Optional(CONF_DEVICE_PORT, default=DEFAULT_DEVICE_PORT): cv.port})
|
vol.Optional(CONF_DEVICE_HOST, default=DEFAULT_DEVICE_HOST): cv.string,
|
||||||
|
vol.Optional(CONF_DEVICE_PORT, default=DEFAULT_DEVICE_PORT): cv.port,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
DEVICE_SERIAL_SCHEMA = vol.Schema({
|
DEVICE_SERIAL_SCHEMA = vol.Schema(
|
||||||
vol.Required(CONF_DEVICE_TYPE): 'serial',
|
{
|
||||||
vol.Optional(CONF_DEVICE_PATH, default=DEFAULT_DEVICE_PATH): cv.string,
|
vol.Required(CONF_DEVICE_TYPE): "serial",
|
||||||
vol.Optional(CONF_DEVICE_BAUD, default=DEFAULT_DEVICE_BAUD): cv.string})
|
vol.Optional(CONF_DEVICE_PATH, default=DEFAULT_DEVICE_PATH): cv.string,
|
||||||
|
vol.Optional(CONF_DEVICE_BAUD, default=DEFAULT_DEVICE_BAUD): cv.string,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
DEVICE_USB_SCHEMA = vol.Schema({
|
DEVICE_USB_SCHEMA = vol.Schema({vol.Required(CONF_DEVICE_TYPE): "usb"})
|
||||||
vol.Required(CONF_DEVICE_TYPE): 'usb'})
|
|
||||||
|
|
||||||
ZONE_SCHEMA = vol.Schema({
|
ZONE_SCHEMA = vol.Schema(
|
||||||
vol.Required(CONF_ZONE_NAME): cv.string,
|
{
|
||||||
vol.Optional(CONF_ZONE_TYPE,
|
vol.Required(CONF_ZONE_NAME): cv.string,
|
||||||
default=DEFAULT_ZONE_TYPE): vol.Any(DEVICE_CLASSES_SCHEMA),
|
vol.Optional(CONF_ZONE_TYPE, default=DEFAULT_ZONE_TYPE): vol.Any(
|
||||||
vol.Optional(CONF_ZONE_RFID): cv.string,
|
DEVICE_CLASSES_SCHEMA
|
||||||
vol.Inclusive(CONF_RELAY_ADDR, 'relaylocation',
|
),
|
||||||
'Relay address and channel must exist together'): cv.byte,
|
vol.Optional(CONF_ZONE_RFID): cv.string,
|
||||||
vol.Inclusive(CONF_RELAY_CHAN, 'relaylocation',
|
vol.Inclusive(
|
||||||
'Relay address and channel must exist together'): cv.byte})
|
CONF_RELAY_ADDR,
|
||||||
|
"relaylocation",
|
||||||
|
"Relay address and channel must exist together",
|
||||||
|
): cv.byte,
|
||||||
|
vol.Inclusive(
|
||||||
|
CONF_RELAY_CHAN,
|
||||||
|
"relaylocation",
|
||||||
|
"Relay address and channel must exist together",
|
||||||
|
): cv.byte,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema({
|
CONFIG_SCHEMA = vol.Schema(
|
||||||
DOMAIN: vol.Schema({
|
{
|
||||||
vol.Required(CONF_DEVICE): vol.Any(
|
DOMAIN: vol.Schema(
|
||||||
DEVICE_SOCKET_SCHEMA, DEVICE_SERIAL_SCHEMA,
|
{
|
||||||
DEVICE_USB_SCHEMA),
|
vol.Required(CONF_DEVICE): vol.Any(
|
||||||
vol.Optional(CONF_PANEL_DISPLAY,
|
DEVICE_SOCKET_SCHEMA, DEVICE_SERIAL_SCHEMA, DEVICE_USB_SCHEMA
|
||||||
default=DEFAULT_PANEL_DISPLAY): cv.boolean,
|
),
|
||||||
vol.Optional(CONF_ZONES): {vol.Coerce(int): ZONE_SCHEMA},
|
vol.Optional(
|
||||||
}),
|
CONF_PANEL_DISPLAY, default=DEFAULT_PANEL_DISPLAY
|
||||||
}, extra=vol.ALLOW_EXTRA)
|
): cv.boolean,
|
||||||
|
vol.Optional(CONF_ZONES): {vol.Coerce(int): ZONE_SCHEMA},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
extra=vol.ALLOW_EXTRA,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def setup(hass, config):
|
def setup(hass, config):
|
||||||
"""Set up for the AlarmDecoder devices."""
|
"""Set up for the AlarmDecoder devices."""
|
||||||
from alarmdecoder import AlarmDecoder
|
from alarmdecoder import AlarmDecoder
|
||||||
from alarmdecoder.devices import (SocketDevice, SerialDevice, USBDevice)
|
from alarmdecoder.devices import SocketDevice, SerialDevice, USBDevice
|
||||||
|
|
||||||
conf = config.get(DOMAIN)
|
conf = config.get(DOMAIN)
|
||||||
|
|
||||||
|
|
@ -120,13 +141,15 @@ def setup(hass, config):
|
||||||
def open_connection(now=None):
|
def open_connection(now=None):
|
||||||
"""Open a connection to AlarmDecoder."""
|
"""Open a connection to AlarmDecoder."""
|
||||||
from alarmdecoder.util import NoDeviceError
|
from alarmdecoder.util import NoDeviceError
|
||||||
|
|
||||||
nonlocal restart
|
nonlocal restart
|
||||||
try:
|
try:
|
||||||
controller.open(baud)
|
controller.open(baud)
|
||||||
except NoDeviceError:
|
except NoDeviceError:
|
||||||
_LOGGER.debug("Failed to connect. Retrying in 5 seconds")
|
_LOGGER.debug("Failed to connect. Retrying in 5 seconds")
|
||||||
hass.helpers.event.track_point_in_time(
|
hass.helpers.event.track_point_in_time(
|
||||||
open_connection, dt_util.utcnow() + timedelta(seconds=5))
|
open_connection, dt_util.utcnow() + timedelta(seconds=5)
|
||||||
|
)
|
||||||
return
|
return
|
||||||
_LOGGER.debug("Established a connection with the alarmdecoder")
|
_LOGGER.debug("Established a connection with the alarmdecoder")
|
||||||
restart = True
|
restart = True
|
||||||
|
|
@ -142,39 +165,34 @@ def setup(hass, config):
|
||||||
|
|
||||||
def handle_message(sender, message):
|
def handle_message(sender, message):
|
||||||
"""Handle message from AlarmDecoder."""
|
"""Handle message from AlarmDecoder."""
|
||||||
hass.helpers.dispatcher.dispatcher_send(
|
hass.helpers.dispatcher.dispatcher_send(SIGNAL_PANEL_MESSAGE, message)
|
||||||
SIGNAL_PANEL_MESSAGE, message)
|
|
||||||
|
|
||||||
def handle_rfx_message(sender, message):
|
def handle_rfx_message(sender, message):
|
||||||
"""Handle RFX message from AlarmDecoder."""
|
"""Handle RFX message from AlarmDecoder."""
|
||||||
hass.helpers.dispatcher.dispatcher_send(
|
hass.helpers.dispatcher.dispatcher_send(SIGNAL_RFX_MESSAGE, message)
|
||||||
SIGNAL_RFX_MESSAGE, message)
|
|
||||||
|
|
||||||
def zone_fault_callback(sender, zone):
|
def zone_fault_callback(sender, zone):
|
||||||
"""Handle zone fault from AlarmDecoder."""
|
"""Handle zone fault from AlarmDecoder."""
|
||||||
hass.helpers.dispatcher.dispatcher_send(
|
hass.helpers.dispatcher.dispatcher_send(SIGNAL_ZONE_FAULT, zone)
|
||||||
SIGNAL_ZONE_FAULT, zone)
|
|
||||||
|
|
||||||
def zone_restore_callback(sender, zone):
|
def zone_restore_callback(sender, zone):
|
||||||
"""Handle zone restore from AlarmDecoder."""
|
"""Handle zone restore from AlarmDecoder."""
|
||||||
hass.helpers.dispatcher.dispatcher_send(
|
hass.helpers.dispatcher.dispatcher_send(SIGNAL_ZONE_RESTORE, zone)
|
||||||
SIGNAL_ZONE_RESTORE, zone)
|
|
||||||
|
|
||||||
def handle_rel_message(sender, message):
|
def handle_rel_message(sender, message):
|
||||||
"""Handle relay message from AlarmDecoder."""
|
"""Handle relay message from AlarmDecoder."""
|
||||||
hass.helpers.dispatcher.dispatcher_send(
|
hass.helpers.dispatcher.dispatcher_send(SIGNAL_REL_MESSAGE, message)
|
||||||
SIGNAL_REL_MESSAGE, message)
|
|
||||||
|
|
||||||
controller = False
|
controller = False
|
||||||
if device_type == 'socket':
|
if device_type == "socket":
|
||||||
host = device.get(CONF_DEVICE_HOST)
|
host = device.get(CONF_DEVICE_HOST)
|
||||||
port = device.get(CONF_DEVICE_PORT)
|
port = device.get(CONF_DEVICE_PORT)
|
||||||
controller = AlarmDecoder(SocketDevice(interface=(host, port)))
|
controller = AlarmDecoder(SocketDevice(interface=(host, port)))
|
||||||
elif device_type == 'serial':
|
elif device_type == "serial":
|
||||||
path = device.get(CONF_DEVICE_PATH)
|
path = device.get(CONF_DEVICE_PATH)
|
||||||
baud = device.get(CONF_DEVICE_BAUD)
|
baud = device.get(CONF_DEVICE_BAUD)
|
||||||
controller = AlarmDecoder(SerialDevice(interface=path))
|
controller = AlarmDecoder(SerialDevice(interface=path))
|
||||||
elif device_type == 'usb':
|
elif device_type == "usb":
|
||||||
AlarmDecoder(USBDevice.find())
|
AlarmDecoder(USBDevice.find())
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
@ -191,13 +209,12 @@ def setup(hass, config):
|
||||||
|
|
||||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_alarmdecoder)
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_alarmdecoder)
|
||||||
|
|
||||||
load_platform(hass, 'alarm_control_panel', DOMAIN, conf, config)
|
load_platform(hass, "alarm_control_panel", DOMAIN, conf, config)
|
||||||
|
|
||||||
if zones:
|
if zones:
|
||||||
load_platform(
|
load_platform(hass, "binary_sensor", DOMAIN, {CONF_ZONES: zones}, config)
|
||||||
hass, 'binary_sensor', DOMAIN, {CONF_ZONES: zones}, config)
|
|
||||||
|
|
||||||
if display:
|
if display:
|
||||||
load_platform(hass, 'sensor', DOMAIN, conf, config)
|
load_platform(hass, "sensor", DOMAIN, conf, config)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
|
||||||
|
|
@ -12,46 +12,54 @@ import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_ENTITY_ID, STATE_IDLE, CONF_NAME, CONF_STATE, STATE_ON, STATE_OFF,
|
CONF_ENTITY_ID,
|
||||||
SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, ATTR_ENTITY_ID)
|
STATE_IDLE,
|
||||||
|
CONF_NAME,
|
||||||
|
CONF_STATE,
|
||||||
|
STATE_ON,
|
||||||
|
STATE_OFF,
|
||||||
|
SERVICE_TURN_ON,
|
||||||
|
SERVICE_TURN_OFF,
|
||||||
|
SERVICE_TOGGLE,
|
||||||
|
ATTR_ENTITY_ID,
|
||||||
|
)
|
||||||
from homeassistant.helpers.entity import ToggleEntity
|
from homeassistant.helpers.entity import ToggleEntity
|
||||||
from homeassistant.helpers import service, event
|
from homeassistant.helpers import service, event
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DOMAIN = 'alert'
|
DOMAIN = "alert"
|
||||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
ENTITY_ID_FORMAT = DOMAIN + ".{}"
|
||||||
|
|
||||||
CONF_DONE_MESSAGE = 'done_message'
|
CONF_DONE_MESSAGE = "done_message"
|
||||||
CONF_CAN_ACK = 'can_acknowledge'
|
CONF_CAN_ACK = "can_acknowledge"
|
||||||
CONF_NOTIFIERS = 'notifiers'
|
CONF_NOTIFIERS = "notifiers"
|
||||||
CONF_REPEAT = 'repeat'
|
CONF_REPEAT = "repeat"
|
||||||
CONF_SKIP_FIRST = 'skip_first'
|
CONF_SKIP_FIRST = "skip_first"
|
||||||
|
|
||||||
DEFAULT_CAN_ACK = True
|
DEFAULT_CAN_ACK = True
|
||||||
DEFAULT_SKIP_FIRST = False
|
DEFAULT_SKIP_FIRST = False
|
||||||
|
|
||||||
ALERT_SCHEMA = vol.Schema({
|
ALERT_SCHEMA = vol.Schema(
|
||||||
vol.Required(CONF_NAME): cv.string,
|
{
|
||||||
vol.Optional(CONF_DONE_MESSAGE): cv.string,
|
vol.Required(CONF_NAME): cv.string,
|
||||||
vol.Required(CONF_ENTITY_ID): cv.entity_id,
|
vol.Optional(CONF_DONE_MESSAGE): cv.string,
|
||||||
vol.Required(CONF_STATE, default=STATE_ON): cv.string,
|
vol.Required(CONF_ENTITY_ID): cv.entity_id,
|
||||||
vol.Required(CONF_REPEAT): vol.All(cv.ensure_list, [vol.Coerce(float)]),
|
vol.Required(CONF_STATE, default=STATE_ON): cv.string,
|
||||||
vol.Required(CONF_CAN_ACK, default=DEFAULT_CAN_ACK): cv.boolean,
|
vol.Required(CONF_REPEAT): vol.All(cv.ensure_list, [vol.Coerce(float)]),
|
||||||
vol.Required(CONF_SKIP_FIRST, default=DEFAULT_SKIP_FIRST): cv.boolean,
|
vol.Required(CONF_CAN_ACK, default=DEFAULT_CAN_ACK): cv.boolean,
|
||||||
vol.Required(CONF_NOTIFIERS): cv.ensure_list})
|
vol.Required(CONF_SKIP_FIRST, default=DEFAULT_SKIP_FIRST): cv.boolean,
|
||||||
|
vol.Required(CONF_NOTIFIERS): cv.ensure_list,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema({
|
CONFIG_SCHEMA = vol.Schema(
|
||||||
DOMAIN: vol.Schema({
|
{DOMAIN: vol.Schema({cv.slug: ALERT_SCHEMA})}, extra=vol.ALLOW_EXTRA
|
||||||
cv.slug: ALERT_SCHEMA,
|
)
|
||||||
}),
|
|
||||||
}, extra=vol.ALLOW_EXTRA)
|
|
||||||
|
|
||||||
|
|
||||||
ALERT_SERVICE_SCHEMA = vol.Schema({
|
ALERT_SERVICE_SCHEMA = vol.Schema({vol.Required(ATTR_ENTITY_ID): cv.entity_ids})
|
||||||
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
def is_on(hass, entity_id):
|
def is_on(hass, entity_id):
|
||||||
|
|
@ -68,8 +76,7 @@ def turn_on(hass, entity_id):
|
||||||
def async_turn_on(hass, entity_id):
|
def async_turn_on(hass, entity_id):
|
||||||
"""Async reset the alert."""
|
"""Async reset the alert."""
|
||||||
data = {ATTR_ENTITY_ID: entity_id}
|
data = {ATTR_ENTITY_ID: entity_id}
|
||||||
hass.async_create_task(
|
hass.async_create_task(hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data))
|
||||||
hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data))
|
|
||||||
|
|
||||||
|
|
||||||
def turn_off(hass, entity_id):
|
def turn_off(hass, entity_id):
|
||||||
|
|
@ -81,8 +88,7 @@ def turn_off(hass, entity_id):
|
||||||
def async_turn_off(hass, entity_id):
|
def async_turn_off(hass, entity_id):
|
||||||
"""Async acknowledge the alert."""
|
"""Async acknowledge the alert."""
|
||||||
data = {ATTR_ENTITY_ID: entity_id}
|
data = {ATTR_ENTITY_ID: entity_id}
|
||||||
hass.async_create_task(
|
hass.async_create_task(hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, data))
|
||||||
hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, data))
|
|
||||||
|
|
||||||
|
|
||||||
def toggle(hass, entity_id):
|
def toggle(hass, entity_id):
|
||||||
|
|
@ -94,8 +100,7 @@ def toggle(hass, entity_id):
|
||||||
def async_toggle(hass, entity_id):
|
def async_toggle(hass, entity_id):
|
||||||
"""Async toggle acknowledgement of alert."""
|
"""Async toggle acknowledgement of alert."""
|
||||||
data = {ATTR_ENTITY_ID: entity_id}
|
data = {ATTR_ENTITY_ID: entity_id}
|
||||||
hass.async_create_task(
|
hass.async_create_task(hass.services.async_call(DOMAIN, SERVICE_TOGGLE, data))
|
||||||
hass.services.async_call(DOMAIN, SERVICE_TOGGLE, data))
|
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
|
|
@ -121,23 +126,33 @@ def async_setup(hass, config):
|
||||||
|
|
||||||
# Setup alerts
|
# Setup alerts
|
||||||
for entity_id, alert in alerts.items():
|
for entity_id, alert in alerts.items():
|
||||||
entity = Alert(hass, entity_id,
|
entity = Alert(
|
||||||
alert[CONF_NAME], alert.get(CONF_DONE_MESSAGE),
|
hass,
|
||||||
alert[CONF_ENTITY_ID], alert[CONF_STATE],
|
entity_id,
|
||||||
alert[CONF_REPEAT], alert[CONF_SKIP_FIRST],
|
alert[CONF_NAME],
|
||||||
alert[CONF_NOTIFIERS], alert[CONF_CAN_ACK])
|
alert.get(CONF_DONE_MESSAGE),
|
||||||
|
alert[CONF_ENTITY_ID],
|
||||||
|
alert[CONF_STATE],
|
||||||
|
alert[CONF_REPEAT],
|
||||||
|
alert[CONF_SKIP_FIRST],
|
||||||
|
alert[CONF_NOTIFIERS],
|
||||||
|
alert[CONF_CAN_ACK],
|
||||||
|
)
|
||||||
all_alerts[entity.entity_id] = entity
|
all_alerts[entity.entity_id] = entity
|
||||||
|
|
||||||
# Setup service calls
|
# Setup service calls
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
DOMAIN, SERVICE_TURN_OFF, async_handle_alert_service,
|
DOMAIN,
|
||||||
schema=ALERT_SERVICE_SCHEMA)
|
SERVICE_TURN_OFF,
|
||||||
|
async_handle_alert_service,
|
||||||
|
schema=ALERT_SERVICE_SCHEMA,
|
||||||
|
)
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
DOMAIN, SERVICE_TURN_ON, async_handle_alert_service,
|
DOMAIN, SERVICE_TURN_ON, async_handle_alert_service, schema=ALERT_SERVICE_SCHEMA
|
||||||
schema=ALERT_SERVICE_SCHEMA)
|
)
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
DOMAIN, SERVICE_TOGGLE, async_handle_alert_service,
|
DOMAIN, SERVICE_TOGGLE, async_handle_alert_service, schema=ALERT_SERVICE_SCHEMA
|
||||||
schema=ALERT_SERVICE_SCHEMA)
|
)
|
||||||
|
|
||||||
tasks = [alert.async_update_ha_state() for alert in all_alerts.values()]
|
tasks = [alert.async_update_ha_state() for alert in all_alerts.values()]
|
||||||
if tasks:
|
if tasks:
|
||||||
|
|
@ -149,8 +164,19 @@ def async_setup(hass, config):
|
||||||
class Alert(ToggleEntity):
|
class Alert(ToggleEntity):
|
||||||
"""Representation of an alert."""
|
"""Representation of an alert."""
|
||||||
|
|
||||||
def __init__(self, hass, entity_id, name, done_message, watched_entity_id,
|
def __init__(
|
||||||
state, repeat, skip_first, notifiers, can_ack):
|
self,
|
||||||
|
hass,
|
||||||
|
entity_id,
|
||||||
|
name,
|
||||||
|
done_message,
|
||||||
|
watched_entity_id,
|
||||||
|
state,
|
||||||
|
repeat,
|
||||||
|
skip_first,
|
||||||
|
notifiers,
|
||||||
|
can_ack,
|
||||||
|
):
|
||||||
"""Initialize the alert."""
|
"""Initialize the alert."""
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self._name = name
|
self._name = name
|
||||||
|
|
@ -170,7 +196,8 @@ class Alert(ToggleEntity):
|
||||||
self.entity_id = ENTITY_ID_FORMAT.format(entity_id)
|
self.entity_id = ENTITY_ID_FORMAT.format(entity_id)
|
||||||
|
|
||||||
event.async_track_state_change(
|
event.async_track_state_change(
|
||||||
hass, watched_entity_id, self.watched_entity_change)
|
hass, watched_entity_id, self.watched_entity_change
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
|
|
@ -236,8 +263,9 @@ class Alert(ToggleEntity):
|
||||||
"""Schedule a notification."""
|
"""Schedule a notification."""
|
||||||
delay = self._delay[self._next_delay]
|
delay = self._delay[self._next_delay]
|
||||||
next_msg = datetime.now() + delay
|
next_msg = datetime.now() + delay
|
||||||
self._cancel = \
|
self._cancel = event.async_track_point_in_time(
|
||||||
event.async_track_point_in_time(self.hass, self._notify, next_msg)
|
self.hass, self._notify, next_msg
|
||||||
|
)
|
||||||
self._next_delay = min(self._next_delay + 1, len(self._delay) - 1)
|
self._next_delay = min(self._next_delay + 1, len(self._delay) - 1)
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
|
|
@ -251,7 +279,8 @@ class Alert(ToggleEntity):
|
||||||
self._send_done_message = True
|
self._send_done_message = True
|
||||||
for target in self._notifiers:
|
for target in self._notifiers:
|
||||||
yield from self.hass.services.async_call(
|
yield from self.hass.services.async_call(
|
||||||
'notify', target, {'message': self._name})
|
"notify", target, {"message": self._name}
|
||||||
|
)
|
||||||
yield from self._schedule_notify()
|
yield from self._schedule_notify()
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
|
|
@ -261,7 +290,8 @@ class Alert(ToggleEntity):
|
||||||
self._send_done_message = False
|
self._send_done_message = False
|
||||||
for target in self._notifiers:
|
for target in self._notifiers:
|
||||||
yield from self.hass.services.async_call(
|
yield from self.hass.services.async_call(
|
||||||
'notify', target, {'message': self._done_message})
|
"notify", target, {"message": self._done_message}
|
||||||
|
)
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_turn_on(self, **kwargs):
|
def async_turn_on(self, **kwargs):
|
||||||
|
|
|
||||||
|
|
@ -14,43 +14,62 @@ from homeassistant.helpers import entityfilter
|
||||||
|
|
||||||
from . import flash_briefings, intent, smart_home
|
from . import flash_briefings, intent, smart_home
|
||||||
from .const import (
|
from .const import (
|
||||||
CONF_AUDIO, CONF_DISPLAY_URL, CONF_TEXT, CONF_TITLE, CONF_UID, DOMAIN,
|
CONF_AUDIO,
|
||||||
CONF_FILTER, CONF_ENTITY_CONFIG)
|
CONF_DISPLAY_URL,
|
||||||
|
CONF_TEXT,
|
||||||
|
CONF_TITLE,
|
||||||
|
CONF_UID,
|
||||||
|
DOMAIN,
|
||||||
|
CONF_FILTER,
|
||||||
|
CONF_ENTITY_CONFIG,
|
||||||
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
CONF_FLASH_BRIEFINGS = 'flash_briefings'
|
CONF_FLASH_BRIEFINGS = "flash_briefings"
|
||||||
CONF_SMART_HOME = 'smart_home'
|
CONF_SMART_HOME = "smart_home"
|
||||||
|
|
||||||
DEPENDENCIES = ['http']
|
DEPENDENCIES = ["http"]
|
||||||
|
|
||||||
ALEXA_ENTITY_SCHEMA = vol.Schema({
|
ALEXA_ENTITY_SCHEMA = vol.Schema(
|
||||||
vol.Optional(smart_home.CONF_DESCRIPTION): cv.string,
|
{
|
||||||
vol.Optional(smart_home.CONF_DISPLAY_CATEGORIES): cv.string,
|
vol.Optional(smart_home.CONF_DESCRIPTION): cv.string,
|
||||||
vol.Optional(smart_home.CONF_NAME): cv.string,
|
vol.Optional(smart_home.CONF_DISPLAY_CATEGORIES): cv.string,
|
||||||
})
|
vol.Optional(smart_home.CONF_NAME): cv.string,
|
||||||
|
|
||||||
SMART_HOME_SCHEMA = vol.Schema({
|
|
||||||
vol.Optional(CONF_FILTER, default={}): entityfilter.FILTER_SCHEMA,
|
|
||||||
vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: ALEXA_ENTITY_SCHEMA}
|
|
||||||
})
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema({
|
|
||||||
DOMAIN: {
|
|
||||||
CONF_FLASH_BRIEFINGS: {
|
|
||||||
cv.string: vol.All(cv.ensure_list, [{
|
|
||||||
vol.Optional(CONF_UID): cv.string,
|
|
||||||
vol.Required(CONF_TITLE): cv.template,
|
|
||||||
vol.Optional(CONF_AUDIO): cv.template,
|
|
||||||
vol.Required(CONF_TEXT, default=""): cv.template,
|
|
||||||
vol.Optional(CONF_DISPLAY_URL): cv.template,
|
|
||||||
}]),
|
|
||||||
},
|
|
||||||
# vol.Optional here would mean we couldn't distinguish between an empty
|
|
||||||
# smart_home: and none at all.
|
|
||||||
CONF_SMART_HOME: vol.Any(SMART_HOME_SCHEMA, None),
|
|
||||||
}
|
}
|
||||||
}, extra=vol.ALLOW_EXTRA)
|
)
|
||||||
|
|
||||||
|
SMART_HOME_SCHEMA = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Optional(CONF_FILTER, default={}): entityfilter.FILTER_SCHEMA,
|
||||||
|
vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: ALEXA_ENTITY_SCHEMA},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = vol.Schema(
|
||||||
|
{
|
||||||
|
DOMAIN: {
|
||||||
|
CONF_FLASH_BRIEFINGS: {
|
||||||
|
cv.string: vol.All(
|
||||||
|
cv.ensure_list,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
vol.Optional(CONF_UID): cv.string,
|
||||||
|
vol.Required(CONF_TITLE): cv.template,
|
||||||
|
vol.Optional(CONF_AUDIO): cv.template,
|
||||||
|
vol.Required(CONF_TEXT, default=""): cv.template,
|
||||||
|
vol.Optional(CONF_DISPLAY_URL): cv.template,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
},
|
||||||
|
# vol.Optional here would mean we couldn't distinguish between an empty
|
||||||
|
# smart_home: and none at all.
|
||||||
|
CONF_SMART_HOME: vol.Any(SMART_HOME_SCHEMA, None),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
extra=vol.ALLOW_EXTRA,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,23 @@
|
||||||
"""Constants for the Alexa integration."""
|
"""Constants for the Alexa integration."""
|
||||||
DOMAIN = 'alexa'
|
DOMAIN = "alexa"
|
||||||
|
|
||||||
# Flash briefing constants
|
# Flash briefing constants
|
||||||
CONF_UID = 'uid'
|
CONF_UID = "uid"
|
||||||
CONF_TITLE = 'title'
|
CONF_TITLE = "title"
|
||||||
CONF_AUDIO = 'audio'
|
CONF_AUDIO = "audio"
|
||||||
CONF_TEXT = 'text'
|
CONF_TEXT = "text"
|
||||||
CONF_DISPLAY_URL = 'display_url'
|
CONF_DISPLAY_URL = "display_url"
|
||||||
|
|
||||||
CONF_FILTER = 'filter'
|
CONF_FILTER = "filter"
|
||||||
CONF_ENTITY_CONFIG = 'entity_config'
|
CONF_ENTITY_CONFIG = "entity_config"
|
||||||
|
|
||||||
ATTR_UID = 'uid'
|
ATTR_UID = "uid"
|
||||||
ATTR_UPDATE_DATE = 'updateDate'
|
ATTR_UPDATE_DATE = "updateDate"
|
||||||
ATTR_TITLE_TEXT = 'titleText'
|
ATTR_TITLE_TEXT = "titleText"
|
||||||
ATTR_STREAM_URL = 'streamUrl'
|
ATTR_STREAM_URL = "streamUrl"
|
||||||
ATTR_MAIN_TEXT = 'mainText'
|
ATTR_MAIN_TEXT = "mainText"
|
||||||
ATTR_REDIRECTION_URL = 'redirectionURL'
|
ATTR_REDIRECTION_URL = "redirectionURL"
|
||||||
|
|
||||||
SYN_RESOLUTION_MATCH = 'ER_SUCCESS_MATCH'
|
SYN_RESOLUTION_MATCH = "ER_SUCCESS_MATCH"
|
||||||
|
|
||||||
DATE_FORMAT = '%Y-%m-%dT%H:%M:%S.0Z'
|
DATE_FORMAT = "%Y-%m-%dT%H:%M:%S.0Z"
|
||||||
|
|
|
||||||
|
|
@ -14,27 +14,36 @@ from homeassistant.core import callback
|
||||||
from homeassistant.helpers import template
|
from homeassistant.helpers import template
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
ATTR_MAIN_TEXT, ATTR_REDIRECTION_URL, ATTR_STREAM_URL, ATTR_TITLE_TEXT,
|
ATTR_MAIN_TEXT,
|
||||||
ATTR_UID, ATTR_UPDATE_DATE, CONF_AUDIO, CONF_DISPLAY_URL, CONF_TEXT,
|
ATTR_REDIRECTION_URL,
|
||||||
CONF_TITLE, CONF_UID, DATE_FORMAT)
|
ATTR_STREAM_URL,
|
||||||
|
ATTR_TITLE_TEXT,
|
||||||
|
ATTR_UID,
|
||||||
|
ATTR_UPDATE_DATE,
|
||||||
|
CONF_AUDIO,
|
||||||
|
CONF_DISPLAY_URL,
|
||||||
|
CONF_TEXT,
|
||||||
|
CONF_TITLE,
|
||||||
|
CONF_UID,
|
||||||
|
DATE_FORMAT,
|
||||||
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
FLASH_BRIEFINGS_API_ENDPOINT = '/api/alexa/flash_briefings/{briefing_id}'
|
FLASH_BRIEFINGS_API_ENDPOINT = "/api/alexa/flash_briefings/{briefing_id}"
|
||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_setup(hass, flash_briefing_config):
|
def async_setup(hass, flash_briefing_config):
|
||||||
"""Activate Alexa component."""
|
"""Activate Alexa component."""
|
||||||
hass.http.register_view(
|
hass.http.register_view(AlexaFlashBriefingView(hass, flash_briefing_config))
|
||||||
AlexaFlashBriefingView(hass, flash_briefing_config))
|
|
||||||
|
|
||||||
|
|
||||||
class AlexaFlashBriefingView(http.HomeAssistantView):
|
class AlexaFlashBriefingView(http.HomeAssistantView):
|
||||||
"""Handle Alexa Flash Briefing skill requests."""
|
"""Handle Alexa Flash Briefing skill requests."""
|
||||||
|
|
||||||
url = FLASH_BRIEFINGS_API_ENDPOINT
|
url = FLASH_BRIEFINGS_API_ENDPOINT
|
||||||
name = 'api:alexa:flash_briefings'
|
name = "api:alexa:flash_briefings"
|
||||||
|
|
||||||
def __init__(self, hass, flash_briefings):
|
def __init__(self, hass, flash_briefings):
|
||||||
"""Initialize Alexa view."""
|
"""Initialize Alexa view."""
|
||||||
|
|
@ -45,13 +54,12 @@ class AlexaFlashBriefingView(http.HomeAssistantView):
|
||||||
@callback
|
@callback
|
||||||
def get(self, request, briefing_id):
|
def get(self, request, briefing_id):
|
||||||
"""Handle Alexa Flash Briefing request."""
|
"""Handle Alexa Flash Briefing request."""
|
||||||
_LOGGER.debug("Received Alexa flash briefing request for: %s",
|
_LOGGER.debug("Received Alexa flash briefing request for: %s", briefing_id)
|
||||||
briefing_id)
|
|
||||||
|
|
||||||
if self.flash_briefings.get(briefing_id) is None:
|
if self.flash_briefings.get(briefing_id) is None:
|
||||||
err = "No configured Alexa flash briefing was found for: %s"
|
err = "No configured Alexa flash briefing was found for: %s"
|
||||||
_LOGGER.error(err, briefing_id)
|
_LOGGER.error(err, briefing_id)
|
||||||
return b'', 404
|
return b"", 404
|
||||||
|
|
||||||
briefing = []
|
briefing = []
|
||||||
|
|
||||||
|
|
@ -81,10 +89,8 @@ class AlexaFlashBriefingView(http.HomeAssistantView):
|
||||||
output[ATTR_STREAM_URL] = item.get(CONF_AUDIO)
|
output[ATTR_STREAM_URL] = item.get(CONF_AUDIO)
|
||||||
|
|
||||||
if item.get(CONF_DISPLAY_URL) is not None:
|
if item.get(CONF_DISPLAY_URL) is not None:
|
||||||
if isinstance(item.get(CONF_DISPLAY_URL),
|
if isinstance(item.get(CONF_DISPLAY_URL), template.Template):
|
||||||
template.Template):
|
output[ATTR_REDIRECTION_URL] = item[CONF_DISPLAY_URL].async_render()
|
||||||
output[ATTR_REDIRECTION_URL] = \
|
|
||||||
item[CONF_DISPLAY_URL].async_render()
|
|
||||||
else:
|
else:
|
||||||
output[ATTR_REDIRECTION_URL] = item.get(CONF_DISPLAY_URL)
|
output[ATTR_REDIRECTION_URL] = item.get(CONF_DISPLAY_URL)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,27 +20,24 @@ _LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
HANDLERS = Registry()
|
HANDLERS = Registry()
|
||||||
|
|
||||||
INTENTS_API_ENDPOINT = '/api/alexa'
|
INTENTS_API_ENDPOINT = "/api/alexa"
|
||||||
|
|
||||||
|
|
||||||
class SpeechType(enum.Enum):
|
class SpeechType(enum.Enum):
|
||||||
"""The Alexa speech types."""
|
"""The Alexa speech types."""
|
||||||
|
|
||||||
plaintext = 'PlainText'
|
plaintext = "PlainText"
|
||||||
ssml = 'SSML'
|
ssml = "SSML"
|
||||||
|
|
||||||
|
|
||||||
SPEECH_MAPPINGS = {
|
SPEECH_MAPPINGS = {"plain": SpeechType.plaintext, "ssml": SpeechType.ssml}
|
||||||
'plain': SpeechType.plaintext,
|
|
||||||
'ssml': SpeechType.ssml,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class CardType(enum.Enum):
|
class CardType(enum.Enum):
|
||||||
"""The Alexa card types."""
|
"""The Alexa card types."""
|
||||||
|
|
||||||
simple = 'Simple'
|
simple = "Simple"
|
||||||
link_account = 'LinkAccount'
|
link_account = "LinkAccount"
|
||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
|
|
@ -57,45 +54,51 @@ class AlexaIntentsView(http.HomeAssistantView):
|
||||||
"""Handle Alexa requests."""
|
"""Handle Alexa requests."""
|
||||||
|
|
||||||
url = INTENTS_API_ENDPOINT
|
url = INTENTS_API_ENDPOINT
|
||||||
name = 'api:alexa'
|
name = "api:alexa"
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
"""Handle Alexa."""
|
"""Handle Alexa."""
|
||||||
hass = request.app['hass']
|
hass = request.app["hass"]
|
||||||
message = yield from request.json()
|
message = yield from request.json()
|
||||||
|
|
||||||
_LOGGER.debug("Received Alexa request: %s", message)
|
_LOGGER.debug("Received Alexa request: %s", message)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = yield from async_handle_message(hass, message)
|
response = yield from async_handle_message(hass, message)
|
||||||
return b'' if response is None else self.json(response)
|
return b"" if response is None else self.json(response)
|
||||||
except UnknownRequest as err:
|
except UnknownRequest as err:
|
||||||
_LOGGER.warning(str(err))
|
_LOGGER.warning(str(err))
|
||||||
return self.json(intent_error_response(
|
return self.json(intent_error_response(hass, message, str(err)))
|
||||||
hass, message, str(err)))
|
|
||||||
|
|
||||||
except intent.UnknownIntent as err:
|
except intent.UnknownIntent as err:
|
||||||
_LOGGER.warning(str(err))
|
_LOGGER.warning(str(err))
|
||||||
return self.json(intent_error_response(
|
return self.json(
|
||||||
hass, message,
|
intent_error_response(
|
||||||
"This intent is not yet configured within Home Assistant."))
|
hass,
|
||||||
|
message,
|
||||||
|
"This intent is not yet configured within Home Assistant.",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
except intent.InvalidSlotInfo as err:
|
except intent.InvalidSlotInfo as err:
|
||||||
_LOGGER.error("Received invalid slot data from Alexa: %s", err)
|
_LOGGER.error("Received invalid slot data from Alexa: %s", err)
|
||||||
return self.json(intent_error_response(
|
return self.json(
|
||||||
hass, message,
|
intent_error_response(
|
||||||
"Invalid slot information received for this intent."))
|
hass, message, "Invalid slot information received for this intent."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
except intent.IntentError as err:
|
except intent.IntentError as err:
|
||||||
_LOGGER.exception(str(err))
|
_LOGGER.exception(str(err))
|
||||||
return self.json(intent_error_response(
|
return self.json(
|
||||||
hass, message, "Error handling intent."))
|
intent_error_response(hass, message, "Error handling intent.")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def intent_error_response(hass, message, error):
|
def intent_error_response(hass, message, error):
|
||||||
"""Return an Alexa response that will speak the error message."""
|
"""Return an Alexa response that will speak the error message."""
|
||||||
alexa_intent_info = message.get('request').get('intent')
|
alexa_intent_info = message.get("request").get("intent")
|
||||||
alexa_response = AlexaResponse(hass, alexa_intent_info)
|
alexa_response = AlexaResponse(hass, alexa_intent_info)
|
||||||
alexa_response.add_speech(SpeechType.plaintext, error)
|
alexa_response.add_speech(SpeechType.plaintext, error)
|
||||||
return alexa_response.as_dict()
|
return alexa_response.as_dict()
|
||||||
|
|
@ -112,26 +115,26 @@ def async_handle_message(hass, message):
|
||||||
- intent.IntentError
|
- intent.IntentError
|
||||||
|
|
||||||
"""
|
"""
|
||||||
req = message.get('request')
|
req = message.get("request")
|
||||||
req_type = req['type']
|
req_type = req["type"]
|
||||||
|
|
||||||
handler = HANDLERS.get(req_type)
|
handler = HANDLERS.get(req_type)
|
||||||
|
|
||||||
if not handler:
|
if not handler:
|
||||||
raise UnknownRequest('Received unknown request {}'.format(req_type))
|
raise UnknownRequest("Received unknown request {}".format(req_type))
|
||||||
|
|
||||||
return (yield from handler(hass, message))
|
return (yield from handler(hass, message))
|
||||||
|
|
||||||
|
|
||||||
@HANDLERS.register('SessionEndedRequest')
|
@HANDLERS.register("SessionEndedRequest")
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_handle_session_end(hass, message):
|
def async_handle_session_end(hass, message):
|
||||||
"""Handle a session end request."""
|
"""Handle a session end request."""
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@HANDLERS.register('IntentRequest')
|
@HANDLERS.register("IntentRequest")
|
||||||
@HANDLERS.register('LaunchRequest')
|
@HANDLERS.register("LaunchRequest")
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_handle_intent(hass, message):
|
def async_handle_intent(hass, message):
|
||||||
"""Handle an intent request.
|
"""Handle an intent request.
|
||||||
|
|
@ -142,33 +145,37 @@ def async_handle_intent(hass, message):
|
||||||
- intent.IntentError
|
- intent.IntentError
|
||||||
|
|
||||||
"""
|
"""
|
||||||
req = message.get('request')
|
req = message.get("request")
|
||||||
alexa_intent_info = req.get('intent')
|
alexa_intent_info = req.get("intent")
|
||||||
alexa_response = AlexaResponse(hass, alexa_intent_info)
|
alexa_response = AlexaResponse(hass, alexa_intent_info)
|
||||||
|
|
||||||
if req['type'] == 'LaunchRequest':
|
if req["type"] == "LaunchRequest":
|
||||||
intent_name = message.get('session', {}) \
|
intent_name = (
|
||||||
.get('application', {}) \
|
message.get("session", {}).get("application", {}).get("applicationId")
|
||||||
.get('applicationId')
|
)
|
||||||
else:
|
else:
|
||||||
intent_name = alexa_intent_info['name']
|
intent_name = alexa_intent_info["name"]
|
||||||
|
|
||||||
intent_response = yield from intent.async_handle(
|
intent_response = yield from intent.async_handle(
|
||||||
hass, DOMAIN, intent_name,
|
hass,
|
||||||
{key: {'value': value} for key, value
|
DOMAIN,
|
||||||
in alexa_response.variables.items()})
|
intent_name,
|
||||||
|
{key: {"value": value} for key, value in alexa_response.variables.items()},
|
||||||
|
)
|
||||||
|
|
||||||
for intent_speech, alexa_speech in SPEECH_MAPPINGS.items():
|
for intent_speech, alexa_speech in SPEECH_MAPPINGS.items():
|
||||||
if intent_speech in intent_response.speech:
|
if intent_speech in intent_response.speech:
|
||||||
alexa_response.add_speech(
|
alexa_response.add_speech(
|
||||||
alexa_speech,
|
alexa_speech, intent_response.speech[intent_speech]["speech"]
|
||||||
intent_response.speech[intent_speech]['speech'])
|
)
|
||||||
break
|
break
|
||||||
|
|
||||||
if 'simple' in intent_response.card:
|
if "simple" in intent_response.card:
|
||||||
alexa_response.add_card(
|
alexa_response.add_card(
|
||||||
CardType.simple, intent_response.card['simple']['title'],
|
CardType.simple,
|
||||||
intent_response.card['simple']['content'])
|
intent_response.card["simple"]["title"],
|
||||||
|
intent_response.card["simple"]["content"],
|
||||||
|
)
|
||||||
|
|
||||||
return alexa_response.as_dict()
|
return alexa_response.as_dict()
|
||||||
|
|
||||||
|
|
@ -178,23 +185,23 @@ def resolve_slot_synonyms(key, request):
|
||||||
# Default to the spoken slot value if more than one or none are found. For
|
# Default to the spoken slot value if more than one or none are found. For
|
||||||
# reference to the request object structure, see the Alexa docs:
|
# reference to the request object structure, see the Alexa docs:
|
||||||
# https://tinyurl.com/ybvm7jhs
|
# https://tinyurl.com/ybvm7jhs
|
||||||
resolved_value = request['value']
|
resolved_value = request["value"]
|
||||||
|
|
||||||
if ('resolutions' in request and
|
if (
|
||||||
'resolutionsPerAuthority' in request['resolutions'] and
|
"resolutions" in request
|
||||||
len(request['resolutions']['resolutionsPerAuthority']) >= 1):
|
and "resolutionsPerAuthority" in request["resolutions"]
|
||||||
|
and len(request["resolutions"]["resolutionsPerAuthority"]) >= 1
|
||||||
|
):
|
||||||
|
|
||||||
# Extract all of the possible values from each authority with a
|
# Extract all of the possible values from each authority with a
|
||||||
# successful match
|
# successful match
|
||||||
possible_values = []
|
possible_values = []
|
||||||
|
|
||||||
for entry in request['resolutions']['resolutionsPerAuthority']:
|
for entry in request["resolutions"]["resolutionsPerAuthority"]:
|
||||||
if entry['status']['code'] != SYN_RESOLUTION_MATCH:
|
if entry["status"]["code"] != SYN_RESOLUTION_MATCH:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
possible_values.extend([item['value']['name']
|
possible_values.extend([item["value"]["name"] for item in entry["values"]])
|
||||||
for item
|
|
||||||
in entry['values']])
|
|
||||||
|
|
||||||
# If there is only one match use the resolved value, otherwise the
|
# If there is only one match use the resolved value, otherwise the
|
||||||
# resolution cannot be determined, so use the spoken slot value
|
# resolution cannot be determined, so use the spoken slot value
|
||||||
|
|
@ -202,9 +209,9 @@ def resolve_slot_synonyms(key, request):
|
||||||
resolved_value = possible_values[0]
|
resolved_value = possible_values[0]
|
||||||
else:
|
else:
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
'Found multiple synonym resolutions for slot value: {%s: %s}',
|
"Found multiple synonym resolutions for slot value: {%s: %s}",
|
||||||
key,
|
key,
|
||||||
request['value']
|
request["value"],
|
||||||
)
|
)
|
||||||
|
|
||||||
return resolved_value
|
return resolved_value
|
||||||
|
|
@ -225,12 +232,12 @@ class AlexaResponse:
|
||||||
|
|
||||||
# Intent is None if request was a LaunchRequest or SessionEndedRequest
|
# Intent is None if request was a LaunchRequest or SessionEndedRequest
|
||||||
if intent_info is not None:
|
if intent_info is not None:
|
||||||
for key, value in intent_info.get('slots', {}).items():
|
for key, value in intent_info.get("slots", {}).items():
|
||||||
# Only include slots with values
|
# Only include slots with values
|
||||||
if 'value' not in value:
|
if "value" not in value:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
_key = key.replace('.', '_')
|
_key = key.replace(".", "_")
|
||||||
|
|
||||||
self.variables[_key] = resolve_slot_synonyms(key, value)
|
self.variables[_key] = resolve_slot_synonyms(key, value)
|
||||||
|
|
||||||
|
|
@ -238,9 +245,7 @@ class AlexaResponse:
|
||||||
"""Add a card to the response."""
|
"""Add a card to the response."""
|
||||||
assert self.card is None
|
assert self.card is None
|
||||||
|
|
||||||
card = {
|
card = {"type": card_type.value}
|
||||||
"type": card_type.value
|
|
||||||
}
|
|
||||||
|
|
||||||
if card_type == CardType.link_account:
|
if card_type == CardType.link_account:
|
||||||
self.card = card
|
self.card = card
|
||||||
|
|
@ -254,43 +259,36 @@ class AlexaResponse:
|
||||||
"""Add speech to the response."""
|
"""Add speech to the response."""
|
||||||
assert self.speech is None
|
assert self.speech is None
|
||||||
|
|
||||||
key = 'ssml' if speech_type == SpeechType.ssml else 'text'
|
key = "ssml" if speech_type == SpeechType.ssml else "text"
|
||||||
|
|
||||||
self.speech = {
|
self.speech = {"type": speech_type.value, key: text}
|
||||||
'type': speech_type.value,
|
|
||||||
key: text
|
|
||||||
}
|
|
||||||
|
|
||||||
def add_reprompt(self, speech_type, text):
|
def add_reprompt(self, speech_type, text):
|
||||||
"""Add reprompt if user does not answer."""
|
"""Add reprompt if user does not answer."""
|
||||||
assert self.reprompt is None
|
assert self.reprompt is None
|
||||||
|
|
||||||
key = 'ssml' if speech_type == SpeechType.ssml else 'text'
|
key = "ssml" if speech_type == SpeechType.ssml else "text"
|
||||||
|
|
||||||
self.reprompt = {
|
self.reprompt = {
|
||||||
'type': speech_type.value,
|
"type": speech_type.value,
|
||||||
key: text.async_render(self.variables)
|
key: text.async_render(self.variables),
|
||||||
}
|
}
|
||||||
|
|
||||||
def as_dict(self):
|
def as_dict(self):
|
||||||
"""Return response in an Alexa valid dict."""
|
"""Return response in an Alexa valid dict."""
|
||||||
response = {
|
response = {"shouldEndSession": self.should_end_session}
|
||||||
'shouldEndSession': self.should_end_session
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.card is not None:
|
if self.card is not None:
|
||||||
response['card'] = self.card
|
response["card"] = self.card
|
||||||
|
|
||||||
if self.speech is not None:
|
if self.speech is not None:
|
||||||
response['outputSpeech'] = self.speech
|
response["outputSpeech"] = self.speech
|
||||||
|
|
||||||
if self.reprompt is not None:
|
if self.reprompt is not None:
|
||||||
response['reprompt'] = {
|
response["reprompt"] = {"outputSpeech": self.reprompt}
|
||||||
'outputSpeech': self.reprompt
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'version': '1.0',
|
"version": "1.0",
|
||||||
'sessionAttributes': self.session_attributes,
|
"sessionAttributes": self.session_attributes,
|
||||||
'response': response,
|
"response": response,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -13,85 +13,100 @@ from requests.exceptions import HTTPError, ConnectTimeout
|
||||||
from requests.exceptions import ConnectionError as ConnectError
|
from requests.exceptions import ConnectionError as ConnectError
|
||||||
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_NAME, CONF_HOST, CONF_PORT, CONF_USERNAME, CONF_PASSWORD,
|
CONF_NAME,
|
||||||
CONF_SENSORS, CONF_SWITCHES, CONF_SCAN_INTERVAL, HTTP_BASIC_AUTHENTICATION)
|
CONF_HOST,
|
||||||
|
CONF_PORT,
|
||||||
|
CONF_USERNAME,
|
||||||
|
CONF_PASSWORD,
|
||||||
|
CONF_SENSORS,
|
||||||
|
CONF_SWITCHES,
|
||||||
|
CONF_SCAN_INTERVAL,
|
||||||
|
HTTP_BASIC_AUTHENTICATION,
|
||||||
|
)
|
||||||
from homeassistant.helpers import discovery
|
from homeassistant.helpers import discovery
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
REQUIREMENTS = ['amcrest==1.2.3']
|
REQUIREMENTS = ["amcrest==1.2.3"]
|
||||||
DEPENDENCIES = ['ffmpeg']
|
DEPENDENCIES = ["ffmpeg"]
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
CONF_AUTHENTICATION = 'authentication'
|
CONF_AUTHENTICATION = "authentication"
|
||||||
CONF_RESOLUTION = 'resolution'
|
CONF_RESOLUTION = "resolution"
|
||||||
CONF_STREAM_SOURCE = 'stream_source'
|
CONF_STREAM_SOURCE = "stream_source"
|
||||||
CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments'
|
CONF_FFMPEG_ARGUMENTS = "ffmpeg_arguments"
|
||||||
|
|
||||||
DEFAULT_NAME = 'Amcrest Camera'
|
DEFAULT_NAME = "Amcrest Camera"
|
||||||
DEFAULT_PORT = 80
|
DEFAULT_PORT = 80
|
||||||
DEFAULT_RESOLUTION = 'high'
|
DEFAULT_RESOLUTION = "high"
|
||||||
DEFAULT_STREAM_SOURCE = 'snapshot'
|
DEFAULT_STREAM_SOURCE = "snapshot"
|
||||||
TIMEOUT = 10
|
TIMEOUT = 10
|
||||||
|
|
||||||
DATA_AMCREST = 'amcrest'
|
DATA_AMCREST = "amcrest"
|
||||||
DOMAIN = 'amcrest'
|
DOMAIN = "amcrest"
|
||||||
|
|
||||||
NOTIFICATION_ID = 'amcrest_notification'
|
NOTIFICATION_ID = "amcrest_notification"
|
||||||
NOTIFICATION_TITLE = 'Amcrest Camera Setup'
|
NOTIFICATION_TITLE = "Amcrest Camera Setup"
|
||||||
|
|
||||||
RESOLUTION_LIST = {
|
RESOLUTION_LIST = {"high": 0, "low": 1}
|
||||||
'high': 0,
|
|
||||||
'low': 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
SCAN_INTERVAL = timedelta(seconds=10)
|
SCAN_INTERVAL = timedelta(seconds=10)
|
||||||
|
|
||||||
AUTHENTICATION_LIST = {
|
AUTHENTICATION_LIST = {"basic": "basic"}
|
||||||
'basic': 'basic'
|
|
||||||
}
|
|
||||||
|
|
||||||
STREAM_SOURCE_LIST = {
|
STREAM_SOURCE_LIST = {"mjpeg": 0, "snapshot": 1, "rtsp": 2}
|
||||||
'mjpeg': 0,
|
|
||||||
'snapshot': 1,
|
|
||||||
'rtsp': 2,
|
|
||||||
}
|
|
||||||
|
|
||||||
# Sensor types are defined like: Name, units, icon
|
# Sensor types are defined like: Name, units, icon
|
||||||
SENSORS = {
|
SENSORS = {
|
||||||
'motion_detector': ['Motion Detected', None, 'mdi:run'],
|
"motion_detector": ["Motion Detected", None, "mdi:run"],
|
||||||
'sdcard': ['SD Used', '%', 'mdi:sd'],
|
"sdcard": ["SD Used", "%", "mdi:sd"],
|
||||||
'ptz_preset': ['PTZ Preset', None, 'mdi:camera-iris'],
|
"ptz_preset": ["PTZ Preset", None, "mdi:camera-iris"],
|
||||||
}
|
}
|
||||||
|
|
||||||
# Switch types are defined like: Name, icon
|
# Switch types are defined like: Name, icon
|
||||||
SWITCHES = {
|
SWITCHES = {
|
||||||
'motion_detection': ['Motion Detection', 'mdi:run-fast'],
|
"motion_detection": ["Motion Detection", "mdi:run-fast"],
|
||||||
'motion_recording': ['Motion Recording', 'mdi:record-rec']
|
"motion_recording": ["Motion Recording", "mdi:record-rec"],
|
||||||
}
|
}
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema({
|
CONFIG_SCHEMA = vol.Schema(
|
||||||
DOMAIN: vol.All(cv.ensure_list, [vol.Schema({
|
{
|
||||||
vol.Required(CONF_HOST): cv.string,
|
DOMAIN: vol.All(
|
||||||
vol.Required(CONF_USERNAME): cv.string,
|
cv.ensure_list,
|
||||||
vol.Required(CONF_PASSWORD): cv.string,
|
[
|
||||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
vol.Schema(
|
||||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
{
|
||||||
vol.Optional(CONF_AUTHENTICATION, default=HTTP_BASIC_AUTHENTICATION):
|
vol.Required(CONF_HOST): cv.string,
|
||||||
vol.All(vol.In(AUTHENTICATION_LIST)),
|
vol.Required(CONF_USERNAME): cv.string,
|
||||||
vol.Optional(CONF_RESOLUTION, default=DEFAULT_RESOLUTION):
|
vol.Required(CONF_PASSWORD): cv.string,
|
||||||
vol.All(vol.In(RESOLUTION_LIST)),
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||||
vol.Optional(CONF_STREAM_SOURCE, default=DEFAULT_STREAM_SOURCE):
|
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||||
vol.All(vol.In(STREAM_SOURCE_LIST)),
|
vol.Optional(
|
||||||
vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string,
|
CONF_AUTHENTICATION, default=HTTP_BASIC_AUTHENTICATION
|
||||||
vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL):
|
): vol.All(vol.In(AUTHENTICATION_LIST)),
|
||||||
cv.time_period,
|
vol.Optional(
|
||||||
vol.Optional(CONF_SENSORS):
|
CONF_RESOLUTION, default=DEFAULT_RESOLUTION
|
||||||
vol.All(cv.ensure_list, [vol.In(SENSORS)]),
|
): vol.All(vol.In(RESOLUTION_LIST)),
|
||||||
vol.Optional(CONF_SWITCHES):
|
vol.Optional(
|
||||||
vol.All(cv.ensure_list, [vol.In(SWITCHES)]),
|
CONF_STREAM_SOURCE, default=DEFAULT_STREAM_SOURCE
|
||||||
})])
|
): vol.All(vol.In(STREAM_SOURCE_LIST)),
|
||||||
}, extra=vol.ALLOW_EXTRA)
|
vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string,
|
||||||
|
vol.Optional(
|
||||||
|
CONF_SCAN_INTERVAL, default=SCAN_INTERVAL
|
||||||
|
): cv.time_period,
|
||||||
|
vol.Optional(CONF_SENSORS): vol.All(
|
||||||
|
cv.ensure_list, [vol.In(SENSORS)]
|
||||||
|
),
|
||||||
|
vol.Optional(CONF_SWITCHES): vol.All(
|
||||||
|
cv.ensure_list, [vol.In(SWITCHES)]
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
},
|
||||||
|
extra=vol.ALLOW_EXTRA,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def setup(hass, config):
|
def setup(hass, config):
|
||||||
|
|
@ -103,21 +118,24 @@ def setup(hass, config):
|
||||||
|
|
||||||
for device in amcrest_cams:
|
for device in amcrest_cams:
|
||||||
try:
|
try:
|
||||||
camera = AmcrestCamera(device.get(CONF_HOST),
|
camera = AmcrestCamera(
|
||||||
device.get(CONF_PORT),
|
device.get(CONF_HOST),
|
||||||
device.get(CONF_USERNAME),
|
device.get(CONF_PORT),
|
||||||
device.get(CONF_PASSWORD)).camera
|
device.get(CONF_USERNAME),
|
||||||
|
device.get(CONF_PASSWORD),
|
||||||
|
).camera
|
||||||
# pylint: disable=pointless-statement
|
# pylint: disable=pointless-statement
|
||||||
camera.current_time
|
camera.current_time
|
||||||
|
|
||||||
except (ConnectError, ConnectTimeout, HTTPError) as ex:
|
except (ConnectError, ConnectTimeout, HTTPError) as ex:
|
||||||
_LOGGER.error("Unable to connect to Amcrest camera: %s", str(ex))
|
_LOGGER.error("Unable to connect to Amcrest camera: %s", str(ex))
|
||||||
hass.components.persistent_notification.create(
|
hass.components.persistent_notification.create(
|
||||||
'Error: {}<br />'
|
"Error: {}<br />"
|
||||||
'You will need to restart hass after fixing.'
|
"You will need to restart hass after fixing."
|
||||||
''.format(ex),
|
"".format(ex),
|
||||||
title=NOTIFICATION_TITLE,
|
title=NOTIFICATION_TITLE,
|
||||||
notification_id=NOTIFICATION_ID)
|
notification_id=NOTIFICATION_ID,
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
ffmpeg_arguments = device.get(CONF_FFMPEG_ARGUMENTS)
|
ffmpeg_arguments = device.get(CONF_FFMPEG_ARGUMENTS)
|
||||||
|
|
@ -139,27 +157,24 @@ def setup(hass, config):
|
||||||
authentication = None
|
authentication = None
|
||||||
|
|
||||||
hass.data[DATA_AMCREST][name] = AmcrestDevice(
|
hass.data[DATA_AMCREST][name] = AmcrestDevice(
|
||||||
camera, name, authentication, ffmpeg_arguments, stream_source,
|
camera, name, authentication, ffmpeg_arguments, stream_source, resolution
|
||||||
resolution)
|
)
|
||||||
|
|
||||||
discovery.load_platform(
|
discovery.load_platform(hass, "camera", DOMAIN, {CONF_NAME: name}, config)
|
||||||
hass, 'camera', DOMAIN, {
|
|
||||||
CONF_NAME: name,
|
|
||||||
}, config)
|
|
||||||
|
|
||||||
if sensors:
|
if sensors:
|
||||||
discovery.load_platform(
|
discovery.load_platform(
|
||||||
hass, 'sensor', DOMAIN, {
|
hass, "sensor", DOMAIN, {CONF_NAME: name, CONF_SENSORS: sensors}, config
|
||||||
CONF_NAME: name,
|
)
|
||||||
CONF_SENSORS: sensors,
|
|
||||||
}, config)
|
|
||||||
|
|
||||||
if switches:
|
if switches:
|
||||||
discovery.load_platform(
|
discovery.load_platform(
|
||||||
hass, 'switch', DOMAIN, {
|
hass,
|
||||||
CONF_NAME: name,
|
"switch",
|
||||||
CONF_SWITCHES: switches
|
DOMAIN,
|
||||||
}, config)
|
{CONF_NAME: name, CONF_SWITCHES: switches},
|
||||||
|
config,
|
||||||
|
)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
@ -167,8 +182,9 @@ def setup(hass, config):
|
||||||
class AmcrestDevice:
|
class AmcrestDevice:
|
||||||
"""Representation of a base Amcrest discovery device."""
|
"""Representation of a base Amcrest discovery device."""
|
||||||
|
|
||||||
def __init__(self, camera, name, authentication, ffmpeg_arguments,
|
def __init__(
|
||||||
stream_source, resolution):
|
self, camera, name, authentication, ffmpeg_arguments, stream_source, resolution
|
||||||
|
):
|
||||||
"""Initialize the entity."""
|
"""Initialize the entity."""
|
||||||
self.device = camera
|
self.device = camera
|
||||||
self.name = name
|
self.name = name
|
||||||
|
|
|
||||||
|
|
@ -12,141 +12,183 @@ import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_NAME, CONF_HOST, CONF_PORT, CONF_USERNAME, CONF_PASSWORD,
|
CONF_NAME,
|
||||||
CONF_SENSORS, CONF_SWITCHES, CONF_TIMEOUT, CONF_SCAN_INTERVAL,
|
CONF_HOST,
|
||||||
CONF_PLATFORM)
|
CONF_PORT,
|
||||||
|
CONF_USERNAME,
|
||||||
|
CONF_PASSWORD,
|
||||||
|
CONF_SENSORS,
|
||||||
|
CONF_SWITCHES,
|
||||||
|
CONF_TIMEOUT,
|
||||||
|
CONF_SCAN_INTERVAL,
|
||||||
|
CONF_PLATFORM,
|
||||||
|
)
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
from homeassistant.helpers import discovery
|
from homeassistant.helpers import discovery
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.dispatcher import (
|
from homeassistant.helpers.dispatcher import (
|
||||||
async_dispatcher_send, async_dispatcher_connect)
|
async_dispatcher_send,
|
||||||
|
async_dispatcher_connect,
|
||||||
|
)
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
from homeassistant.helpers.event import async_track_point_in_utc_time
|
from homeassistant.helpers.event import async_track_point_in_utc_time
|
||||||
from homeassistant.util.dt import utcnow
|
from homeassistant.util.dt import utcnow
|
||||||
from homeassistant.components.camera.mjpeg import (
|
from homeassistant.components.camera.mjpeg import CONF_MJPEG_URL, CONF_STILL_IMAGE_URL
|
||||||
CONF_MJPEG_URL, CONF_STILL_IMAGE_URL)
|
|
||||||
|
|
||||||
REQUIREMENTS = ['pydroid-ipcam==0.8']
|
REQUIREMENTS = ["pydroid-ipcam==0.8"]
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
ATTR_AUD_CONNS = 'Audio Connections'
|
ATTR_AUD_CONNS = "Audio Connections"
|
||||||
ATTR_HOST = 'host'
|
ATTR_HOST = "host"
|
||||||
ATTR_VID_CONNS = 'Video Connections'
|
ATTR_VID_CONNS = "Video Connections"
|
||||||
|
|
||||||
CONF_MOTION_SENSOR = 'motion_sensor'
|
CONF_MOTION_SENSOR = "motion_sensor"
|
||||||
|
|
||||||
DATA_IP_WEBCAM = 'android_ip_webcam'
|
DATA_IP_WEBCAM = "android_ip_webcam"
|
||||||
DEFAULT_NAME = 'IP Webcam'
|
DEFAULT_NAME = "IP Webcam"
|
||||||
DEFAULT_PORT = 8080
|
DEFAULT_PORT = 8080
|
||||||
DEFAULT_TIMEOUT = 10
|
DEFAULT_TIMEOUT = 10
|
||||||
DOMAIN = 'android_ip_webcam'
|
DOMAIN = "android_ip_webcam"
|
||||||
|
|
||||||
SCAN_INTERVAL = timedelta(seconds=10)
|
SCAN_INTERVAL = timedelta(seconds=10)
|
||||||
SIGNAL_UPDATE_DATA = 'android_ip_webcam_update'
|
SIGNAL_UPDATE_DATA = "android_ip_webcam_update"
|
||||||
|
|
||||||
KEY_MAP = {
|
KEY_MAP = {
|
||||||
'audio_connections': 'Audio Connections',
|
"audio_connections": "Audio Connections",
|
||||||
'adet_limit': 'Audio Trigger Limit',
|
"adet_limit": "Audio Trigger Limit",
|
||||||
'antibanding': 'Anti-banding',
|
"antibanding": "Anti-banding",
|
||||||
'audio_only': 'Audio Only',
|
"audio_only": "Audio Only",
|
||||||
'battery_level': 'Battery Level',
|
"battery_level": "Battery Level",
|
||||||
'battery_temp': 'Battery Temperature',
|
"battery_temp": "Battery Temperature",
|
||||||
'battery_voltage': 'Battery Voltage',
|
"battery_voltage": "Battery Voltage",
|
||||||
'coloreffect': 'Color Effect',
|
"coloreffect": "Color Effect",
|
||||||
'exposure': 'Exposure Level',
|
"exposure": "Exposure Level",
|
||||||
'exposure_lock': 'Exposure Lock',
|
"exposure_lock": "Exposure Lock",
|
||||||
'ffc': 'Front-facing Camera',
|
"ffc": "Front-facing Camera",
|
||||||
'flashmode': 'Flash Mode',
|
"flashmode": "Flash Mode",
|
||||||
'focus': 'Focus',
|
"focus": "Focus",
|
||||||
'focus_homing': 'Focus Homing',
|
"focus_homing": "Focus Homing",
|
||||||
'focus_region': 'Focus Region',
|
"focus_region": "Focus Region",
|
||||||
'focusmode': 'Focus Mode',
|
"focusmode": "Focus Mode",
|
||||||
'gps_active': 'GPS Active',
|
"gps_active": "GPS Active",
|
||||||
'idle': 'Idle',
|
"idle": "Idle",
|
||||||
'ip_address': 'IPv4 Address',
|
"ip_address": "IPv4 Address",
|
||||||
'ipv6_address': 'IPv6 Address',
|
"ipv6_address": "IPv6 Address",
|
||||||
'ivideon_streaming': 'Ivideon Streaming',
|
"ivideon_streaming": "Ivideon Streaming",
|
||||||
'light': 'Light Level',
|
"light": "Light Level",
|
||||||
'mirror_flip': 'Mirror Flip',
|
"mirror_flip": "Mirror Flip",
|
||||||
'motion': 'Motion',
|
"motion": "Motion",
|
||||||
'motion_active': 'Motion Active',
|
"motion_active": "Motion Active",
|
||||||
'motion_detect': 'Motion Detection',
|
"motion_detect": "Motion Detection",
|
||||||
'motion_event': 'Motion Event',
|
"motion_event": "Motion Event",
|
||||||
'motion_limit': 'Motion Limit',
|
"motion_limit": "Motion Limit",
|
||||||
'night_vision': 'Night Vision',
|
"night_vision": "Night Vision",
|
||||||
'night_vision_average': 'Night Vision Average',
|
"night_vision_average": "Night Vision Average",
|
||||||
'night_vision_gain': 'Night Vision Gain',
|
"night_vision_gain": "Night Vision Gain",
|
||||||
'orientation': 'Orientation',
|
"orientation": "Orientation",
|
||||||
'overlay': 'Overlay',
|
"overlay": "Overlay",
|
||||||
'photo_size': 'Photo Size',
|
"photo_size": "Photo Size",
|
||||||
'pressure': 'Pressure',
|
"pressure": "Pressure",
|
||||||
'proximity': 'Proximity',
|
"proximity": "Proximity",
|
||||||
'quality': 'Quality',
|
"quality": "Quality",
|
||||||
'scenemode': 'Scene Mode',
|
"scenemode": "Scene Mode",
|
||||||
'sound': 'Sound',
|
"sound": "Sound",
|
||||||
'sound_event': 'Sound Event',
|
"sound_event": "Sound Event",
|
||||||
'sound_timeout': 'Sound Timeout',
|
"sound_timeout": "Sound Timeout",
|
||||||
'torch': 'Torch',
|
"torch": "Torch",
|
||||||
'video_connections': 'Video Connections',
|
"video_connections": "Video Connections",
|
||||||
'video_chunk_len': 'Video Chunk Length',
|
"video_chunk_len": "Video Chunk Length",
|
||||||
'video_recording': 'Video Recording',
|
"video_recording": "Video Recording",
|
||||||
'video_size': 'Video Size',
|
"video_size": "Video Size",
|
||||||
'whitebalance': 'White Balance',
|
"whitebalance": "White Balance",
|
||||||
'whitebalance_lock': 'White Balance Lock',
|
"whitebalance_lock": "White Balance Lock",
|
||||||
'zoom': 'Zoom'
|
"zoom": "Zoom",
|
||||||
}
|
}
|
||||||
|
|
||||||
ICON_MAP = {
|
ICON_MAP = {
|
||||||
'audio_connections': 'mdi:speaker',
|
"audio_connections": "mdi:speaker",
|
||||||
'battery_level': 'mdi:battery',
|
"battery_level": "mdi:battery",
|
||||||
'battery_temp': 'mdi:thermometer',
|
"battery_temp": "mdi:thermometer",
|
||||||
'battery_voltage': 'mdi:battery-charging-100',
|
"battery_voltage": "mdi:battery-charging-100",
|
||||||
'exposure_lock': 'mdi:camera',
|
"exposure_lock": "mdi:camera",
|
||||||
'ffc': 'mdi:camera-front-variant',
|
"ffc": "mdi:camera-front-variant",
|
||||||
'focus': 'mdi:image-filter-center-focus',
|
"focus": "mdi:image-filter-center-focus",
|
||||||
'gps_active': 'mdi:crosshairs-gps',
|
"gps_active": "mdi:crosshairs-gps",
|
||||||
'light': 'mdi:flashlight',
|
"light": "mdi:flashlight",
|
||||||
'motion': 'mdi:run',
|
"motion": "mdi:run",
|
||||||
'night_vision': 'mdi:weather-night',
|
"night_vision": "mdi:weather-night",
|
||||||
'overlay': 'mdi:monitor',
|
"overlay": "mdi:monitor",
|
||||||
'pressure': 'mdi:gauge',
|
"pressure": "mdi:gauge",
|
||||||
'proximity': 'mdi:map-marker-radius',
|
"proximity": "mdi:map-marker-radius",
|
||||||
'quality': 'mdi:quality-high',
|
"quality": "mdi:quality-high",
|
||||||
'sound': 'mdi:speaker',
|
"sound": "mdi:speaker",
|
||||||
'sound_event': 'mdi:speaker',
|
"sound_event": "mdi:speaker",
|
||||||
'sound_timeout': 'mdi:speaker',
|
"sound_timeout": "mdi:speaker",
|
||||||
'torch': 'mdi:white-balance-sunny',
|
"torch": "mdi:white-balance-sunny",
|
||||||
'video_chunk_len': 'mdi:video',
|
"video_chunk_len": "mdi:video",
|
||||||
'video_connections': 'mdi:eye',
|
"video_connections": "mdi:eye",
|
||||||
'video_recording': 'mdi:record-rec',
|
"video_recording": "mdi:record-rec",
|
||||||
'whitebalance_lock': 'mdi:white-balance-auto'
|
"whitebalance_lock": "mdi:white-balance-auto",
|
||||||
}
|
}
|
||||||
|
|
||||||
SWITCHES = ['exposure_lock', 'ffc', 'focus', 'gps_active', 'night_vision',
|
SWITCHES = [
|
||||||
'overlay', 'torch', 'whitebalance_lock', 'video_recording']
|
"exposure_lock",
|
||||||
|
"ffc",
|
||||||
|
"focus",
|
||||||
|
"gps_active",
|
||||||
|
"night_vision",
|
||||||
|
"overlay",
|
||||||
|
"torch",
|
||||||
|
"whitebalance_lock",
|
||||||
|
"video_recording",
|
||||||
|
]
|
||||||
|
|
||||||
SENSORS = ['audio_connections', 'battery_level', 'battery_temp',
|
SENSORS = [
|
||||||
'battery_voltage', 'light', 'motion', 'pressure', 'proximity',
|
"audio_connections",
|
||||||
'sound', 'video_connections']
|
"battery_level",
|
||||||
|
"battery_temp",
|
||||||
|
"battery_voltage",
|
||||||
|
"light",
|
||||||
|
"motion",
|
||||||
|
"pressure",
|
||||||
|
"proximity",
|
||||||
|
"sound",
|
||||||
|
"video_connections",
|
||||||
|
]
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema({
|
CONFIG_SCHEMA = vol.Schema(
|
||||||
DOMAIN: vol.All(cv.ensure_list, [vol.Schema({
|
{
|
||||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
DOMAIN: vol.All(
|
||||||
vol.Required(CONF_HOST): cv.string,
|
cv.ensure_list,
|
||||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
[
|
||||||
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
|
vol.Schema(
|
||||||
vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL):
|
{
|
||||||
cv.time_period,
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||||
vol.Inclusive(CONF_USERNAME, 'authentication'): cv.string,
|
vol.Required(CONF_HOST): cv.string,
|
||||||
vol.Inclusive(CONF_PASSWORD, 'authentication'): cv.string,
|
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||||
vol.Optional(CONF_SWITCHES):
|
vol.Optional(
|
||||||
vol.All(cv.ensure_list, [vol.In(SWITCHES)]),
|
CONF_TIMEOUT, default=DEFAULT_TIMEOUT
|
||||||
vol.Optional(CONF_SENSORS):
|
): cv.positive_int,
|
||||||
vol.All(cv.ensure_list, [vol.In(SENSORS)]),
|
vol.Optional(
|
||||||
vol.Optional(CONF_MOTION_SENSOR): cv.boolean,
|
CONF_SCAN_INTERVAL, default=SCAN_INTERVAL
|
||||||
})])
|
): cv.time_period,
|
||||||
}, extra=vol.ALLOW_EXTRA)
|
vol.Inclusive(CONF_USERNAME, "authentication"): cv.string,
|
||||||
|
vol.Inclusive(CONF_PASSWORD, "authentication"): cv.string,
|
||||||
|
vol.Optional(CONF_SWITCHES): vol.All(
|
||||||
|
cv.ensure_list, [vol.In(SWITCHES)]
|
||||||
|
),
|
||||||
|
vol.Optional(CONF_SENSORS): vol.All(
|
||||||
|
cv.ensure_list, [vol.In(SENSORS)]
|
||||||
|
),
|
||||||
|
vol.Optional(CONF_MOTION_SENSOR): cv.boolean,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
},
|
||||||
|
extra=vol.ALLOW_EXTRA,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
|
|
@ -171,22 +213,26 @@ def async_setup(hass, config):
|
||||||
|
|
||||||
# Init ip webcam
|
# Init ip webcam
|
||||||
cam = PyDroidIPCam(
|
cam = PyDroidIPCam(
|
||||||
hass.loop, websession, host, cam_config[CONF_PORT],
|
hass.loop,
|
||||||
username=username, password=password,
|
websession,
|
||||||
timeout=cam_config[CONF_TIMEOUT]
|
host,
|
||||||
|
cam_config[CONF_PORT],
|
||||||
|
username=username,
|
||||||
|
password=password,
|
||||||
|
timeout=cam_config[CONF_TIMEOUT],
|
||||||
)
|
)
|
||||||
|
|
||||||
if switches is None:
|
if switches is None:
|
||||||
switches = [setting for setting in cam.enabled_settings
|
switches = [
|
||||||
if setting in SWITCHES]
|
setting for setting in cam.enabled_settings if setting in SWITCHES
|
||||||
|
]
|
||||||
|
|
||||||
if sensors is None:
|
if sensors is None:
|
||||||
sensors = [sensor for sensor in cam.enabled_sensors
|
sensors = [sensor for sensor in cam.enabled_sensors if sensor in SENSORS]
|
||||||
if sensor in SENSORS]
|
sensors.extend(["audio_connections", "video_connections"])
|
||||||
sensors.extend(['audio_connections', 'video_connections'])
|
|
||||||
|
|
||||||
if motion is None:
|
if motion is None:
|
||||||
motion = 'motion_active' in cam.enabled_sensors
|
motion = "motion_active" in cam.enabled_sensors
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_update_data(now):
|
def async_update_data(now):
|
||||||
|
|
@ -194,8 +240,7 @@ def async_setup(hass, config):
|
||||||
yield from cam.update()
|
yield from cam.update()
|
||||||
async_dispatcher_send(hass, SIGNAL_UPDATE_DATA, host)
|
async_dispatcher_send(hass, SIGNAL_UPDATE_DATA, host)
|
||||||
|
|
||||||
async_track_point_in_utc_time(
|
async_track_point_in_utc_time(hass, async_update_data, utcnow() + interval)
|
||||||
hass, async_update_data, utcnow() + interval)
|
|
||||||
|
|
||||||
yield from async_update_data(None)
|
yield from async_update_data(None)
|
||||||
|
|
||||||
|
|
@ -203,42 +248,50 @@ def async_setup(hass, config):
|
||||||
webcams[host] = cam
|
webcams[host] = cam
|
||||||
|
|
||||||
mjpeg_camera = {
|
mjpeg_camera = {
|
||||||
CONF_PLATFORM: 'mjpeg',
|
CONF_PLATFORM: "mjpeg",
|
||||||
CONF_MJPEG_URL: cam.mjpeg_url,
|
CONF_MJPEG_URL: cam.mjpeg_url,
|
||||||
CONF_STILL_IMAGE_URL: cam.image_url,
|
CONF_STILL_IMAGE_URL: cam.image_url,
|
||||||
CONF_NAME: name,
|
CONF_NAME: name,
|
||||||
}
|
}
|
||||||
if username and password:
|
if username and password:
|
||||||
mjpeg_camera.update({
|
mjpeg_camera.update({CONF_USERNAME: username, CONF_PASSWORD: password})
|
||||||
CONF_USERNAME: username,
|
|
||||||
CONF_PASSWORD: password
|
|
||||||
})
|
|
||||||
|
|
||||||
hass.async_create_task(discovery.async_load_platform(
|
hass.async_create_task(
|
||||||
hass, 'camera', 'mjpeg', mjpeg_camera, config))
|
discovery.async_load_platform(hass, "camera", "mjpeg", mjpeg_camera, config)
|
||||||
|
)
|
||||||
|
|
||||||
if sensors:
|
if sensors:
|
||||||
hass.async_create_task(discovery.async_load_platform(
|
hass.async_create_task(
|
||||||
hass, 'sensor', DOMAIN, {
|
discovery.async_load_platform(
|
||||||
CONF_NAME: name,
|
hass,
|
||||||
CONF_HOST: host,
|
"sensor",
|
||||||
CONF_SENSORS: sensors,
|
DOMAIN,
|
||||||
}, config))
|
{CONF_NAME: name, CONF_HOST: host, CONF_SENSORS: sensors},
|
||||||
|
config,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if switches:
|
if switches:
|
||||||
hass.async_create_task(discovery.async_load_platform(
|
hass.async_create_task(
|
||||||
hass, 'switch', DOMAIN, {
|
discovery.async_load_platform(
|
||||||
CONF_NAME: name,
|
hass,
|
||||||
CONF_HOST: host,
|
"switch",
|
||||||
CONF_SWITCHES: switches,
|
DOMAIN,
|
||||||
}, config))
|
{CONF_NAME: name, CONF_HOST: host, CONF_SWITCHES: switches},
|
||||||
|
config,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if motion:
|
if motion:
|
||||||
hass.async_create_task(discovery.async_load_platform(
|
hass.async_create_task(
|
||||||
hass, 'binary_sensor', DOMAIN, {
|
discovery.async_load_platform(
|
||||||
CONF_HOST: host,
|
hass,
|
||||||
CONF_NAME: name,
|
"binary_sensor",
|
||||||
}, config))
|
DOMAIN,
|
||||||
|
{CONF_HOST: host, CONF_NAME: name},
|
||||||
|
config,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
tasks = [async_setup_ipcamera(conf) for conf in config[DOMAIN]]
|
tasks = [async_setup_ipcamera(conf) for conf in config[DOMAIN]]
|
||||||
if tasks:
|
if tasks:
|
||||||
|
|
@ -258,6 +311,7 @@ class AndroidIPCamEntity(Entity):
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_added_to_hass(self):
|
def async_added_to_hass(self):
|
||||||
"""Register update dispatcher."""
|
"""Register update dispatcher."""
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_ipcam_update(host):
|
def async_ipcam_update(host):
|
||||||
"""Update callback."""
|
"""Update callback."""
|
||||||
|
|
@ -265,8 +319,7 @@ class AndroidIPCamEntity(Entity):
|
||||||
return
|
return
|
||||||
self.async_schedule_update_ha_state(True)
|
self.async_schedule_update_ha_state(True)
|
||||||
|
|
||||||
async_dispatcher_connect(
|
async_dispatcher_connect(self.hass, SIGNAL_UPDATE_DATA, async_ipcam_update)
|
||||||
self.hass, SIGNAL_UPDATE_DATA, async_ipcam_update)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def should_poll(self):
|
def should_poll(self):
|
||||||
|
|
@ -285,9 +338,7 @@ class AndroidIPCamEntity(Entity):
|
||||||
if self._ipcam.status_data is None:
|
if self._ipcam.status_data is None:
|
||||||
return state_attr
|
return state_attr
|
||||||
|
|
||||||
state_attr[ATTR_VID_CONNS] = \
|
state_attr[ATTR_VID_CONNS] = self._ipcam.status_data.get("video_connections")
|
||||||
self._ipcam.status_data.get('video_connections')
|
state_attr[ATTR_AUD_CONNS] = self._ipcam.status_data.get("audio_connections")
|
||||||
state_attr[ATTR_AUD_CONNS] = \
|
|
||||||
self._ipcam.status_data.get('audio_connections')
|
|
||||||
|
|
||||||
return state_attr
|
return state_attr
|
||||||
|
|
|
||||||
|
|
@ -9,33 +9,38 @@ from datetime import timedelta
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.const import (CONF_HOST, CONF_PORT)
|
from homeassistant.const import CONF_HOST, CONF_PORT
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.util import Throttle
|
from homeassistant.util import Throttle
|
||||||
|
|
||||||
REQUIREMENTS = ['apcaccess==0.0.13']
|
REQUIREMENTS = ["apcaccess==0.0.13"]
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
CONF_TYPE = 'type'
|
CONF_TYPE = "type"
|
||||||
|
|
||||||
DATA = None
|
DATA = None
|
||||||
DEFAULT_HOST = 'localhost'
|
DEFAULT_HOST = "localhost"
|
||||||
DEFAULT_PORT = 3551
|
DEFAULT_PORT = 3551
|
||||||
DOMAIN = 'apcupsd'
|
DOMAIN = "apcupsd"
|
||||||
|
|
||||||
KEY_STATUS = 'STATUS'
|
KEY_STATUS = "STATUS"
|
||||||
|
|
||||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
|
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
|
||||||
|
|
||||||
VALUE_ONLINE = 'ONLINE'
|
VALUE_ONLINE = "ONLINE"
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema({
|
CONFIG_SCHEMA = vol.Schema(
|
||||||
DOMAIN: vol.Schema({
|
{
|
||||||
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
|
DOMAIN: vol.Schema(
|
||||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
{
|
||||||
}),
|
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
|
||||||
}, extra=vol.ALLOW_EXTRA)
|
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
extra=vol.ALLOW_EXTRA,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def setup(hass, config):
|
def setup(hass, config):
|
||||||
|
|
@ -68,6 +73,7 @@ class APCUPSdData:
|
||||||
def __init__(self, host, port):
|
def __init__(self, host, port):
|
||||||
"""Initialize the data object."""
|
"""Initialize the data object."""
|
||||||
from apcaccess import status
|
from apcaccess import status
|
||||||
|
|
||||||
self._host = host
|
self._host = host
|
||||||
self._port = port
|
self._port = port
|
||||||
self._status = None
|
self._status = None
|
||||||
|
|
|
||||||
|
|
@ -14,11 +14,25 @@ import async_timeout
|
||||||
from homeassistant.bootstrap import DATA_LOGGING
|
from homeassistant.bootstrap import DATA_LOGGING
|
||||||
from homeassistant.components.http import HomeAssistantView
|
from homeassistant.components.http import HomeAssistantView
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
EVENT_HOMEASSISTANT_STOP, EVENT_TIME_CHANGED, HTTP_BAD_REQUEST,
|
EVENT_HOMEASSISTANT_STOP,
|
||||||
HTTP_CREATED, HTTP_NOT_FOUND, MATCH_ALL, URL_API, URL_API_COMPONENTS,
|
EVENT_TIME_CHANGED,
|
||||||
URL_API_CONFIG, URL_API_DISCOVERY_INFO, URL_API_ERROR_LOG, URL_API_EVENTS,
|
HTTP_BAD_REQUEST,
|
||||||
URL_API_SERVICES, URL_API_STATES, URL_API_STATES_ENTITY, URL_API_STREAM,
|
HTTP_CREATED,
|
||||||
URL_API_TEMPLATE, __version__)
|
HTTP_NOT_FOUND,
|
||||||
|
MATCH_ALL,
|
||||||
|
URL_API,
|
||||||
|
URL_API_COMPONENTS,
|
||||||
|
URL_API_CONFIG,
|
||||||
|
URL_API_DISCOVERY_INFO,
|
||||||
|
URL_API_ERROR_LOG,
|
||||||
|
URL_API_EVENTS,
|
||||||
|
URL_API_SERVICES,
|
||||||
|
URL_API_STATES,
|
||||||
|
URL_API_STATES_ENTITY,
|
||||||
|
URL_API_STREAM,
|
||||||
|
URL_API_TEMPLATE,
|
||||||
|
__version__,
|
||||||
|
)
|
||||||
import homeassistant.core as ha
|
import homeassistant.core as ha
|
||||||
from homeassistant.exceptions import TemplateError
|
from homeassistant.exceptions import TemplateError
|
||||||
from homeassistant.helpers import template
|
from homeassistant.helpers import template
|
||||||
|
|
@ -28,15 +42,15 @@ from homeassistant.helpers.json import JSONEncoder
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
ATTR_BASE_URL = 'base_url'
|
ATTR_BASE_URL = "base_url"
|
||||||
ATTR_LOCATION_NAME = 'location_name'
|
ATTR_LOCATION_NAME = "location_name"
|
||||||
ATTR_REQUIRES_API_PASSWORD = 'requires_api_password'
|
ATTR_REQUIRES_API_PASSWORD = "requires_api_password"
|
||||||
ATTR_VERSION = 'version'
|
ATTR_VERSION = "version"
|
||||||
|
|
||||||
DOMAIN = 'api'
|
DOMAIN = "api"
|
||||||
DEPENDENCIES = ['http']
|
DEPENDENCIES = ["http"]
|
||||||
|
|
||||||
STREAM_PING_PAYLOAD = 'ping'
|
STREAM_PING_PAYLOAD = "ping"
|
||||||
STREAM_PING_INTERVAL = 50 # seconds
|
STREAM_PING_INTERVAL = 50 # seconds
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -65,7 +79,7 @@ class APIStatusView(HomeAssistantView):
|
||||||
"""View to handle Status requests."""
|
"""View to handle Status requests."""
|
||||||
|
|
||||||
url = URL_API
|
url = URL_API
|
||||||
name = 'api:status'
|
name = "api:status"
|
||||||
|
|
||||||
@ha.callback
|
@ha.callback
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
|
|
@ -77,17 +91,17 @@ class APIEventStream(HomeAssistantView):
|
||||||
"""View to handle EventStream requests."""
|
"""View to handle EventStream requests."""
|
||||||
|
|
||||||
url = URL_API_STREAM
|
url = URL_API_STREAM
|
||||||
name = 'api:stream'
|
name = "api:stream"
|
||||||
|
|
||||||
async def get(self, request):
|
async def get(self, request):
|
||||||
"""Provide a streaming interface for the event bus."""
|
"""Provide a streaming interface for the event bus."""
|
||||||
hass = request.app['hass']
|
hass = request.app["hass"]
|
||||||
stop_obj = object()
|
stop_obj = object()
|
||||||
to_write = asyncio.Queue(loop=hass.loop)
|
to_write = asyncio.Queue(loop=hass.loop)
|
||||||
|
|
||||||
restrict = request.query.get('restrict')
|
restrict = request.query.get("restrict")
|
||||||
if restrict:
|
if restrict:
|
||||||
restrict = restrict.split(',') + [EVENT_HOMEASSISTANT_STOP]
|
restrict = restrict.split(",") + [EVENT_HOMEASSISTANT_STOP]
|
||||||
|
|
||||||
async def forward_events(event):
|
async def forward_events(event):
|
||||||
"""Forward events to the open request."""
|
"""Forward events to the open request."""
|
||||||
|
|
@ -107,7 +121,7 @@ class APIEventStream(HomeAssistantView):
|
||||||
await to_write.put(data)
|
await to_write.put(data)
|
||||||
|
|
||||||
response = web.StreamResponse()
|
response = web.StreamResponse()
|
||||||
response.content_type = 'text/event-stream'
|
response.content_type = "text/event-stream"
|
||||||
await response.prepare(request)
|
await response.prepare(request)
|
||||||
|
|
||||||
unsub_stream = hass.bus.async_listen(MATCH_ALL, forward_events)
|
unsub_stream = hass.bus.async_listen(MATCH_ALL, forward_events)
|
||||||
|
|
@ -120,17 +134,15 @@ class APIEventStream(HomeAssistantView):
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
with async_timeout.timeout(STREAM_PING_INTERVAL,
|
with async_timeout.timeout(STREAM_PING_INTERVAL, loop=hass.loop):
|
||||||
loop=hass.loop):
|
|
||||||
payload = await to_write.get()
|
payload = await to_write.get()
|
||||||
|
|
||||||
if payload is stop_obj:
|
if payload is stop_obj:
|
||||||
break
|
break
|
||||||
|
|
||||||
msg = "data: {}\n\n".format(payload)
|
msg = "data: {}\n\n".format(payload)
|
||||||
_LOGGER.debug(
|
_LOGGER.debug("STREAM %s WRITING %s", id(stop_obj), msg.strip())
|
||||||
"STREAM %s WRITING %s", id(stop_obj), msg.strip())
|
await response.write(msg.encode("UTF-8"))
|
||||||
await response.write(msg.encode('UTF-8'))
|
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
await to_write.put(STREAM_PING_PAYLOAD)
|
await to_write.put(STREAM_PING_PAYLOAD)
|
||||||
|
|
||||||
|
|
@ -146,12 +158,12 @@ class APIConfigView(HomeAssistantView):
|
||||||
"""View to handle Configuration requests."""
|
"""View to handle Configuration requests."""
|
||||||
|
|
||||||
url = URL_API_CONFIG
|
url = URL_API_CONFIG
|
||||||
name = 'api:config'
|
name = "api:config"
|
||||||
|
|
||||||
@ha.callback
|
@ha.callback
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
"""Get current configuration."""
|
"""Get current configuration."""
|
||||||
return self.json(request.app['hass'].config.as_dict())
|
return self.json(request.app["hass"].config.as_dict())
|
||||||
|
|
||||||
|
|
||||||
class APIDiscoveryView(HomeAssistantView):
|
class APIDiscoveryView(HomeAssistantView):
|
||||||
|
|
@ -159,19 +171,21 @@ class APIDiscoveryView(HomeAssistantView):
|
||||||
|
|
||||||
requires_auth = False
|
requires_auth = False
|
||||||
url = URL_API_DISCOVERY_INFO
|
url = URL_API_DISCOVERY_INFO
|
||||||
name = 'api:discovery'
|
name = "api:discovery"
|
||||||
|
|
||||||
@ha.callback
|
@ha.callback
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
"""Get discovery information."""
|
"""Get discovery information."""
|
||||||
hass = request.app['hass']
|
hass = request.app["hass"]
|
||||||
needs_auth = hass.config.api.api_password is not None
|
needs_auth = hass.config.api.api_password is not None
|
||||||
return self.json({
|
return self.json(
|
||||||
ATTR_BASE_URL: hass.config.api.base_url,
|
{
|
||||||
ATTR_LOCATION_NAME: hass.config.location_name,
|
ATTR_BASE_URL: hass.config.api.base_url,
|
||||||
ATTR_REQUIRES_API_PASSWORD: needs_auth,
|
ATTR_LOCATION_NAME: hass.config.location_name,
|
||||||
ATTR_VERSION: __version__,
|
ATTR_REQUIRES_API_PASSWORD: needs_auth,
|
||||||
})
|
ATTR_VERSION: __version__,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class APIStatesView(HomeAssistantView):
|
class APIStatesView(HomeAssistantView):
|
||||||
|
|
@ -183,58 +197,58 @@ class APIStatesView(HomeAssistantView):
|
||||||
@ha.callback
|
@ha.callback
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
"""Get current states."""
|
"""Get current states."""
|
||||||
return self.json(request.app['hass'].states.async_all())
|
return self.json(request.app["hass"].states.async_all())
|
||||||
|
|
||||||
|
|
||||||
class APIEntityStateView(HomeAssistantView):
|
class APIEntityStateView(HomeAssistantView):
|
||||||
"""View to handle EntityState requests."""
|
"""View to handle EntityState requests."""
|
||||||
|
|
||||||
url = '/api/states/{entity_id}'
|
url = "/api/states/{entity_id}"
|
||||||
name = 'api:entity-state'
|
name = "api:entity-state"
|
||||||
|
|
||||||
@ha.callback
|
@ha.callback
|
||||||
def get(self, request, entity_id):
|
def get(self, request, entity_id):
|
||||||
"""Retrieve state of entity."""
|
"""Retrieve state of entity."""
|
||||||
state = request.app['hass'].states.get(entity_id)
|
state = request.app["hass"].states.get(entity_id)
|
||||||
if state:
|
if state:
|
||||||
return self.json(state)
|
return self.json(state)
|
||||||
return self.json_message("Entity not found.", HTTP_NOT_FOUND)
|
return self.json_message("Entity not found.", HTTP_NOT_FOUND)
|
||||||
|
|
||||||
async def post(self, request, entity_id):
|
async def post(self, request, entity_id):
|
||||||
"""Update state of entity."""
|
"""Update state of entity."""
|
||||||
hass = request.app['hass']
|
hass = request.app["hass"]
|
||||||
try:
|
try:
|
||||||
data = await request.json()
|
data = await request.json()
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return self.json_message(
|
return self.json_message("Invalid JSON specified.", HTTP_BAD_REQUEST)
|
||||||
"Invalid JSON specified.", HTTP_BAD_REQUEST)
|
|
||||||
|
|
||||||
new_state = data.get('state')
|
new_state = data.get("state")
|
||||||
|
|
||||||
if new_state is None:
|
if new_state is None:
|
||||||
return self.json_message("No state specified.", HTTP_BAD_REQUEST)
|
return self.json_message("No state specified.", HTTP_BAD_REQUEST)
|
||||||
|
|
||||||
attributes = data.get('attributes')
|
attributes = data.get("attributes")
|
||||||
force_update = data.get('force_update', False)
|
force_update = data.get("force_update", False)
|
||||||
|
|
||||||
is_new_state = hass.states.get(entity_id) is None
|
is_new_state = hass.states.get(entity_id) is None
|
||||||
|
|
||||||
# Write state
|
# Write state
|
||||||
hass.states.async_set(entity_id, new_state, attributes, force_update,
|
hass.states.async_set(
|
||||||
self.context(request))
|
entity_id, new_state, attributes, force_update, self.context(request)
|
||||||
|
)
|
||||||
|
|
||||||
# Read the state back for our response
|
# Read the state back for our response
|
||||||
status_code = HTTP_CREATED if is_new_state else 200
|
status_code = HTTP_CREATED if is_new_state else 200
|
||||||
resp = self.json(hass.states.get(entity_id), status_code)
|
resp = self.json(hass.states.get(entity_id), status_code)
|
||||||
|
|
||||||
resp.headers.add('Location', URL_API_STATES_ENTITY.format(entity_id))
|
resp.headers.add("Location", URL_API_STATES_ENTITY.format(entity_id))
|
||||||
|
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
@ha.callback
|
@ha.callback
|
||||||
def delete(self, request, entity_id):
|
def delete(self, request, entity_id):
|
||||||
"""Remove entity."""
|
"""Remove entity."""
|
||||||
if request.app['hass'].states.async_remove(entity_id):
|
if request.app["hass"].states.async_remove(entity_id):
|
||||||
return self.json_message("Entity removed.")
|
return self.json_message("Entity removed.")
|
||||||
return self.json_message("Entity not found.", HTTP_NOT_FOUND)
|
return self.json_message("Entity not found.", HTTP_NOT_FOUND)
|
||||||
|
|
||||||
|
|
@ -243,19 +257,19 @@ class APIEventListenersView(HomeAssistantView):
|
||||||
"""View to handle EventListeners requests."""
|
"""View to handle EventListeners requests."""
|
||||||
|
|
||||||
url = URL_API_EVENTS
|
url = URL_API_EVENTS
|
||||||
name = 'api:event-listeners'
|
name = "api:event-listeners"
|
||||||
|
|
||||||
@ha.callback
|
@ha.callback
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
"""Get event listeners."""
|
"""Get event listeners."""
|
||||||
return self.json(async_events_json(request.app['hass']))
|
return self.json(async_events_json(request.app["hass"]))
|
||||||
|
|
||||||
|
|
||||||
class APIEventView(HomeAssistantView):
|
class APIEventView(HomeAssistantView):
|
||||||
"""View to handle Event requests."""
|
"""View to handle Event requests."""
|
||||||
|
|
||||||
url = '/api/events/{event_type}'
|
url = "/api/events/{event_type}"
|
||||||
name = 'api:event'
|
name = "api:event"
|
||||||
|
|
||||||
async def post(self, request, event_type):
|
async def post(self, request, event_type):
|
||||||
"""Fire events."""
|
"""Fire events."""
|
||||||
|
|
@ -264,24 +278,26 @@ class APIEventView(HomeAssistantView):
|
||||||
event_data = json.loads(body) if body else None
|
event_data = json.loads(body) if body else None
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return self.json_message(
|
return self.json_message(
|
||||||
"Event data should be valid JSON.", HTTP_BAD_REQUEST)
|
"Event data should be valid JSON.", HTTP_BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
if event_data is not None and not isinstance(event_data, dict):
|
if event_data is not None and not isinstance(event_data, dict):
|
||||||
return self.json_message(
|
return self.json_message(
|
||||||
"Event data should be a JSON object", HTTP_BAD_REQUEST)
|
"Event data should be a JSON object", HTTP_BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
# Special case handling for event STATE_CHANGED
|
# Special case handling for event STATE_CHANGED
|
||||||
# We will try to convert state dicts back to State objects
|
# We will try to convert state dicts back to State objects
|
||||||
if event_type == ha.EVENT_STATE_CHANGED and event_data:
|
if event_type == ha.EVENT_STATE_CHANGED and event_data:
|
||||||
for key in ('old_state', 'new_state'):
|
for key in ("old_state", "new_state"):
|
||||||
state = ha.State.from_dict(event_data.get(key))
|
state = ha.State.from_dict(event_data.get(key))
|
||||||
|
|
||||||
if state:
|
if state:
|
||||||
event_data[key] = state
|
event_data[key] = state
|
||||||
|
|
||||||
request.app['hass'].bus.async_fire(
|
request.app["hass"].bus.async_fire(
|
||||||
event_type, event_data, ha.EventOrigin.remote,
|
event_type, event_data, ha.EventOrigin.remote, self.context(request)
|
||||||
self.context(request))
|
)
|
||||||
|
|
||||||
return self.json_message("Event {} fired.".format(event_type))
|
return self.json_message("Event {} fired.".format(event_type))
|
||||||
|
|
||||||
|
|
@ -290,36 +306,36 @@ class APIServicesView(HomeAssistantView):
|
||||||
"""View to handle Services requests."""
|
"""View to handle Services requests."""
|
||||||
|
|
||||||
url = URL_API_SERVICES
|
url = URL_API_SERVICES
|
||||||
name = 'api:services'
|
name = "api:services"
|
||||||
|
|
||||||
async def get(self, request):
|
async def get(self, request):
|
||||||
"""Get registered services."""
|
"""Get registered services."""
|
||||||
services = await async_services_json(request.app['hass'])
|
services = await async_services_json(request.app["hass"])
|
||||||
return self.json(services)
|
return self.json(services)
|
||||||
|
|
||||||
|
|
||||||
class APIDomainServicesView(HomeAssistantView):
|
class APIDomainServicesView(HomeAssistantView):
|
||||||
"""View to handle DomainServices requests."""
|
"""View to handle DomainServices requests."""
|
||||||
|
|
||||||
url = '/api/services/{domain}/{service}'
|
url = "/api/services/{domain}/{service}"
|
||||||
name = 'api:domain-services'
|
name = "api:domain-services"
|
||||||
|
|
||||||
async def post(self, request, domain, service):
|
async def post(self, request, domain, service):
|
||||||
"""Call a service.
|
"""Call a service.
|
||||||
|
|
||||||
Returns a list of changed states.
|
Returns a list of changed states.
|
||||||
"""
|
"""
|
||||||
hass = request.app['hass']
|
hass = request.app["hass"]
|
||||||
body = await request.text()
|
body = await request.text()
|
||||||
try:
|
try:
|
||||||
data = json.loads(body) if body else None
|
data = json.loads(body) if body else None
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return self.json_message(
|
return self.json_message("Data should be valid JSON.", HTTP_BAD_REQUEST)
|
||||||
"Data should be valid JSON.", HTTP_BAD_REQUEST)
|
|
||||||
|
|
||||||
with AsyncTrackStates(hass) as changed_states:
|
with AsyncTrackStates(hass) as changed_states:
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
domain, service, data, True, self.context(request))
|
domain, service, data, True, self.context(request)
|
||||||
|
)
|
||||||
|
|
||||||
return self.json(changed_states)
|
return self.json(changed_states)
|
||||||
|
|
||||||
|
|
@ -328,50 +344,52 @@ class APIComponentsView(HomeAssistantView):
|
||||||
"""View to handle Components requests."""
|
"""View to handle Components requests."""
|
||||||
|
|
||||||
url = URL_API_COMPONENTS
|
url = URL_API_COMPONENTS
|
||||||
name = 'api:components'
|
name = "api:components"
|
||||||
|
|
||||||
@ha.callback
|
@ha.callback
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
"""Get current loaded components."""
|
"""Get current loaded components."""
|
||||||
return self.json(request.app['hass'].config.components)
|
return self.json(request.app["hass"].config.components)
|
||||||
|
|
||||||
|
|
||||||
class APITemplateView(HomeAssistantView):
|
class APITemplateView(HomeAssistantView):
|
||||||
"""View to handle Template requests."""
|
"""View to handle Template requests."""
|
||||||
|
|
||||||
url = URL_API_TEMPLATE
|
url = URL_API_TEMPLATE
|
||||||
name = 'api:template'
|
name = "api:template"
|
||||||
|
|
||||||
async def post(self, request):
|
async def post(self, request):
|
||||||
"""Render a template."""
|
"""Render a template."""
|
||||||
try:
|
try:
|
||||||
data = await request.json()
|
data = await request.json()
|
||||||
tpl = template.Template(data['template'], request.app['hass'])
|
tpl = template.Template(data["template"], request.app["hass"])
|
||||||
return tpl.async_render(data.get('variables'))
|
return tpl.async_render(data.get("variables"))
|
||||||
except (ValueError, TemplateError) as ex:
|
except (ValueError, TemplateError) as ex:
|
||||||
return self.json_message(
|
return self.json_message(
|
||||||
"Error rendering template: {}".format(ex), HTTP_BAD_REQUEST)
|
"Error rendering template: {}".format(ex), HTTP_BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class APIErrorLog(HomeAssistantView):
|
class APIErrorLog(HomeAssistantView):
|
||||||
"""View to fetch the API error log."""
|
"""View to fetch the API error log."""
|
||||||
|
|
||||||
url = URL_API_ERROR_LOG
|
url = URL_API_ERROR_LOG
|
||||||
name = 'api:error_log'
|
name = "api:error_log"
|
||||||
|
|
||||||
async def get(self, request):
|
async def get(self, request):
|
||||||
"""Retrieve API error log."""
|
"""Retrieve API error log."""
|
||||||
return web.FileResponse(request.app['hass'].data[DATA_LOGGING])
|
return web.FileResponse(request.app["hass"].data[DATA_LOGGING])
|
||||||
|
|
||||||
|
|
||||||
async def async_services_json(hass):
|
async def async_services_json(hass):
|
||||||
"""Generate services data to JSONify."""
|
"""Generate services data to JSONify."""
|
||||||
descriptions = await async_get_all_descriptions(hass)
|
descriptions = await async_get_all_descriptions(hass)
|
||||||
return [{'domain': key, 'services': value}
|
return [{"domain": key, "services": value} for key, value in descriptions.items()]
|
||||||
for key, value in descriptions.items()]
|
|
||||||
|
|
||||||
|
|
||||||
def async_events_json(hass):
|
def async_events_json(hass):
|
||||||
"""Generate event data to JSONify."""
|
"""Generate event data to JSONify."""
|
||||||
return [{'event': key, 'listener_count': value}
|
return [
|
||||||
for key, value in hass.bus.async_listeners().items()]
|
{"event": key, "listener_count": value}
|
||||||
|
for key, value in hass.bus.async_listeners().items()
|
||||||
|
]
|
||||||
|
|
|
||||||
|
|
@ -16,35 +16,35 @@ from homeassistant.helpers import discovery
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
REQUIREMENTS = ['pyatv==0.3.10']
|
REQUIREMENTS = ["pyatv==0.3.10"]
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DOMAIN = 'apple_tv'
|
DOMAIN = "apple_tv"
|
||||||
|
|
||||||
SERVICE_SCAN = 'apple_tv_scan'
|
SERVICE_SCAN = "apple_tv_scan"
|
||||||
SERVICE_AUTHENTICATE = 'apple_tv_authenticate'
|
SERVICE_AUTHENTICATE = "apple_tv_authenticate"
|
||||||
|
|
||||||
ATTR_ATV = 'atv'
|
ATTR_ATV = "atv"
|
||||||
ATTR_POWER = 'power'
|
ATTR_POWER = "power"
|
||||||
|
|
||||||
CONF_LOGIN_ID = 'login_id'
|
CONF_LOGIN_ID = "login_id"
|
||||||
CONF_START_OFF = 'start_off'
|
CONF_START_OFF = "start_off"
|
||||||
CONF_CREDENTIALS = 'credentials'
|
CONF_CREDENTIALS = "credentials"
|
||||||
|
|
||||||
DEFAULT_NAME = 'Apple TV'
|
DEFAULT_NAME = "Apple TV"
|
||||||
|
|
||||||
DATA_APPLE_TV = 'data_apple_tv'
|
DATA_APPLE_TV = "data_apple_tv"
|
||||||
DATA_ENTITIES = 'data_apple_tv_entities'
|
DATA_ENTITIES = "data_apple_tv_entities"
|
||||||
|
|
||||||
KEY_CONFIG = 'apple_tv_configuring'
|
KEY_CONFIG = "apple_tv_configuring"
|
||||||
|
|
||||||
NOTIFICATION_AUTH_ID = 'apple_tv_auth_notification'
|
NOTIFICATION_AUTH_ID = "apple_tv_auth_notification"
|
||||||
NOTIFICATION_AUTH_TITLE = 'Apple TV Authentication'
|
NOTIFICATION_AUTH_TITLE = "Apple TV Authentication"
|
||||||
NOTIFICATION_SCAN_ID = 'apple_tv_scan_notification'
|
NOTIFICATION_SCAN_ID = "apple_tv_scan_notification"
|
||||||
NOTIFICATION_SCAN_TITLE = 'Apple TV Scan'
|
NOTIFICATION_SCAN_TITLE = "Apple TV Scan"
|
||||||
|
|
||||||
T = TypeVar('T') # pylint: disable=invalid-name
|
T = TypeVar("T") # pylint: disable=invalid-name
|
||||||
|
|
||||||
|
|
||||||
# This version of ensure_list interprets an empty dict as no value
|
# This version of ensure_list interprets an empty dict as no value
|
||||||
|
|
@ -55,22 +55,30 @@ def ensure_list(value: Union[T, Sequence[T]]) -> Sequence[T]:
|
||||||
return value if isinstance(value, list) else [value]
|
return value if isinstance(value, list) else [value]
|
||||||
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema({
|
CONFIG_SCHEMA = vol.Schema(
|
||||||
DOMAIN: vol.All(ensure_list, [vol.Schema({
|
{
|
||||||
vol.Required(CONF_HOST): cv.string,
|
DOMAIN: vol.All(
|
||||||
vol.Required(CONF_LOGIN_ID): cv.string,
|
ensure_list,
|
||||||
vol.Optional(CONF_CREDENTIALS): cv.string,
|
[
|
||||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
vol.Schema(
|
||||||
vol.Optional(CONF_START_OFF, default=False): cv.boolean,
|
{
|
||||||
})])
|
vol.Required(CONF_HOST): cv.string,
|
||||||
}, extra=vol.ALLOW_EXTRA)
|
vol.Required(CONF_LOGIN_ID): cv.string,
|
||||||
|
vol.Optional(CONF_CREDENTIALS): cv.string,
|
||||||
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||||
|
vol.Optional(CONF_START_OFF, default=False): cv.boolean,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
},
|
||||||
|
extra=vol.ALLOW_EXTRA,
|
||||||
|
)
|
||||||
|
|
||||||
# Currently no attributes but it might change later
|
# Currently no attributes but it might change later
|
||||||
APPLE_TV_SCAN_SCHEMA = vol.Schema({})
|
APPLE_TV_SCAN_SCHEMA = vol.Schema({})
|
||||||
|
|
||||||
APPLE_TV_AUTHENTICATE_SCHEMA = vol.Schema({
|
APPLE_TV_AUTHENTICATE_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids})
|
||||||
ATTR_ENTITY_ID: cv.entity_ids,
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
def request_configuration(hass, config, atv, credentials):
|
def request_configuration(hass, config, atv, credentials):
|
||||||
|
|
@ -81,30 +89,34 @@ def request_configuration(hass, config, atv, credentials):
|
||||||
def configuration_callback(callback_data):
|
def configuration_callback(callback_data):
|
||||||
"""Handle the submitted configuration."""
|
"""Handle the submitted configuration."""
|
||||||
from pyatv import exceptions
|
from pyatv import exceptions
|
||||||
pin = callback_data.get('pin')
|
|
||||||
|
pin = callback_data.get("pin")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
yield from atv.airplay.finish_authentication(pin)
|
yield from atv.airplay.finish_authentication(pin)
|
||||||
hass.components.persistent_notification.async_create(
|
hass.components.persistent_notification.async_create(
|
||||||
'Authentication succeeded!<br /><br />Add the following '
|
"Authentication succeeded!<br /><br />Add the following "
|
||||||
'to credentials: in your apple_tv configuration:<br /><br />'
|
"to credentials: in your apple_tv configuration:<br /><br />"
|
||||||
'{0}'.format(credentials),
|
"{0}".format(credentials),
|
||||||
title=NOTIFICATION_AUTH_TITLE,
|
title=NOTIFICATION_AUTH_TITLE,
|
||||||
notification_id=NOTIFICATION_AUTH_ID)
|
notification_id=NOTIFICATION_AUTH_ID,
|
||||||
|
)
|
||||||
except exceptions.DeviceAuthenticationError as ex:
|
except exceptions.DeviceAuthenticationError as ex:
|
||||||
hass.components.persistent_notification.async_create(
|
hass.components.persistent_notification.async_create(
|
||||||
'Authentication failed! Did you enter correct PIN?<br /><br />'
|
"Authentication failed! Did you enter correct PIN?<br /><br />"
|
||||||
'Details: {0}'.format(ex),
|
"Details: {0}".format(ex),
|
||||||
title=NOTIFICATION_AUTH_TITLE,
|
title=NOTIFICATION_AUTH_TITLE,
|
||||||
notification_id=NOTIFICATION_AUTH_ID)
|
notification_id=NOTIFICATION_AUTH_ID,
|
||||||
|
)
|
||||||
|
|
||||||
hass.async_add_job(configurator.request_done, instance)
|
hass.async_add_job(configurator.request_done, instance)
|
||||||
|
|
||||||
instance = configurator.request_config(
|
instance = configurator.request_config(
|
||||||
'Apple TV Authentication', configuration_callback,
|
"Apple TV Authentication",
|
||||||
description='Please enter PIN code shown on screen.',
|
configuration_callback,
|
||||||
submit_caption='Confirm',
|
description="Please enter PIN code shown on screen.",
|
||||||
fields=[{'id': 'pin', 'name': 'PIN Code', 'type': 'password'}]
|
submit_caption="Confirm",
|
||||||
|
fields=[{"id": "pin", "name": "PIN Code", "type": "password"}],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -112,24 +124,28 @@ def request_configuration(hass, config, atv, credentials):
|
||||||
def scan_for_apple_tvs(hass):
|
def scan_for_apple_tvs(hass):
|
||||||
"""Scan for devices and present a notification of the ones found."""
|
"""Scan for devices and present a notification of the ones found."""
|
||||||
import pyatv
|
import pyatv
|
||||||
|
|
||||||
atvs = yield from pyatv.scan_for_apple_tvs(hass.loop, timeout=3)
|
atvs = yield from pyatv.scan_for_apple_tvs(hass.loop, timeout=3)
|
||||||
|
|
||||||
devices = []
|
devices = []
|
||||||
for atv in atvs:
|
for atv in atvs:
|
||||||
login_id = atv.login_id
|
login_id = atv.login_id
|
||||||
if login_id is None:
|
if login_id is None:
|
||||||
login_id = 'Home Sharing disabled'
|
login_id = "Home Sharing disabled"
|
||||||
devices.append('Name: {0}<br />Host: {1}<br />Login ID: {2}'.format(
|
devices.append(
|
||||||
atv.name, atv.address, login_id))
|
"Name: {0}<br />Host: {1}<br />Login ID: {2}".format(
|
||||||
|
atv.name, atv.address, login_id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if not devices:
|
if not devices:
|
||||||
devices = ['No device(s) found']
|
devices = ["No device(s) found"]
|
||||||
|
|
||||||
hass.components.persistent_notification.async_create(
|
hass.components.persistent_notification.async_create(
|
||||||
'The following devices were found:<br /><br />' +
|
"The following devices were found:<br /><br />" + "<br /><br />".join(devices),
|
||||||
'<br /><br />'.join(devices),
|
|
||||||
title=NOTIFICATION_SCAN_TITLE,
|
title=NOTIFICATION_SCAN_TITLE,
|
||||||
notification_id=NOTIFICATION_SCAN_ID)
|
notification_id=NOTIFICATION_SCAN_ID,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
|
|
@ -148,8 +164,11 @@ def async_setup(hass, config):
|
||||||
return
|
return
|
||||||
|
|
||||||
if entity_ids:
|
if entity_ids:
|
||||||
devices = [device for device in hass.data[DATA_ENTITIES]
|
devices = [
|
||||||
if device.entity_id in entity_ids]
|
device
|
||||||
|
for device in hass.data[DATA_ENTITIES]
|
||||||
|
if device.entity_id in entity_ids
|
||||||
|
]
|
||||||
else:
|
else:
|
||||||
devices = hass.data[DATA_ENTITIES]
|
devices = hass.data[DATA_ENTITIES]
|
||||||
|
|
||||||
|
|
@ -160,20 +179,22 @@ def async_setup(hass, config):
|
||||||
atv = device.atv
|
atv = device.atv
|
||||||
credentials = yield from atv.airplay.generate_credentials()
|
credentials = yield from atv.airplay.generate_credentials()
|
||||||
yield from atv.airplay.load_credentials(credentials)
|
yield from atv.airplay.load_credentials(credentials)
|
||||||
_LOGGER.debug('Generated new credentials: %s', credentials)
|
_LOGGER.debug("Generated new credentials: %s", credentials)
|
||||||
yield from atv.airplay.start_authentication()
|
yield from atv.airplay.start_authentication()
|
||||||
hass.async_add_job(request_configuration,
|
hass.async_add_job(request_configuration, hass, config, atv, credentials)
|
||||||
hass, config, atv, credentials)
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def atv_discovered(service, info):
|
def atv_discovered(service, info):
|
||||||
"""Set up an Apple TV that was auto discovered."""
|
"""Set up an Apple TV that was auto discovered."""
|
||||||
yield from _setup_atv(hass, {
|
yield from _setup_atv(
|
||||||
CONF_NAME: info['name'],
|
hass,
|
||||||
CONF_HOST: info['host'],
|
{
|
||||||
CONF_LOGIN_ID: info['properties']['hG'],
|
CONF_NAME: info["name"],
|
||||||
CONF_START_OFF: False
|
CONF_HOST: info["host"],
|
||||||
})
|
CONF_LOGIN_ID: info["properties"]["hG"],
|
||||||
|
CONF_START_OFF: False,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
discovery.async_listen(hass, SERVICE_APPLE_TV, atv_discovered)
|
discovery.async_listen(hass, SERVICE_APPLE_TV, atv_discovered)
|
||||||
|
|
||||||
|
|
@ -182,12 +203,15 @@ def async_setup(hass, config):
|
||||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||||
|
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
DOMAIN, SERVICE_SCAN, async_service_handler,
|
DOMAIN, SERVICE_SCAN, async_service_handler, schema=APPLE_TV_SCAN_SCHEMA
|
||||||
schema=APPLE_TV_SCAN_SCHEMA)
|
)
|
||||||
|
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
DOMAIN, SERVICE_AUTHENTICATE, async_service_handler,
|
DOMAIN,
|
||||||
schema=APPLE_TV_AUTHENTICATE_SCHEMA)
|
SERVICE_AUTHENTICATE,
|
||||||
|
async_service_handler,
|
||||||
|
schema=APPLE_TV_AUTHENTICATE_SCHEMA,
|
||||||
|
)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
@ -196,6 +220,7 @@ def async_setup(hass, config):
|
||||||
def _setup_atv(hass, atv_config):
|
def _setup_atv(hass, atv_config):
|
||||||
"""Set up an Apple TV."""
|
"""Set up an Apple TV."""
|
||||||
import pyatv
|
import pyatv
|
||||||
|
|
||||||
name = atv_config.get(CONF_NAME)
|
name = atv_config.get(CONF_NAME)
|
||||||
host = atv_config.get(CONF_HOST)
|
host = atv_config.get(CONF_HOST)
|
||||||
login_id = atv_config.get(CONF_LOGIN_ID)
|
login_id = atv_config.get(CONF_LOGIN_ID)
|
||||||
|
|
@ -212,16 +237,15 @@ def _setup_atv(hass, atv_config):
|
||||||
yield from atv.airplay.load_credentials(credentials)
|
yield from atv.airplay.load_credentials(credentials)
|
||||||
|
|
||||||
power = AppleTVPowerManager(hass, atv, start_off)
|
power = AppleTVPowerManager(hass, atv, start_off)
|
||||||
hass.data[DATA_APPLE_TV][host] = {
|
hass.data[DATA_APPLE_TV][host] = {ATTR_ATV: atv, ATTR_POWER: power}
|
||||||
ATTR_ATV: atv,
|
|
||||||
ATTR_POWER: power
|
|
||||||
}
|
|
||||||
|
|
||||||
hass.async_create_task(discovery.async_load_platform(
|
hass.async_create_task(
|
||||||
hass, 'media_player', DOMAIN, atv_config))
|
discovery.async_load_platform(hass, "media_player", DOMAIN, atv_config)
|
||||||
|
)
|
||||||
|
|
||||||
hass.async_create_task(discovery.async_load_platform(
|
hass.async_create_task(
|
||||||
hass, 'remote', DOMAIN, atv_config))
|
discovery.async_load_platform(hass, "remote", DOMAIN, atv_config)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AppleTVPowerManager:
|
class AppleTVPowerManager:
|
||||||
|
|
|
||||||
|
|
@ -8,24 +8,21 @@ import logging
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP
|
||||||
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
|
|
||||||
from homeassistant.const import CONF_PORT
|
from homeassistant.const import CONF_PORT
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
REQUIREMENTS = ['PyMata==2.14']
|
REQUIREMENTS = ["PyMata==2.14"]
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
BOARD = None
|
BOARD = None
|
||||||
|
|
||||||
DOMAIN = 'arduino'
|
DOMAIN = "arduino"
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema({
|
CONFIG_SCHEMA = vol.Schema(
|
||||||
DOMAIN: vol.Schema({
|
{DOMAIN: vol.Schema({vol.Required(CONF_PORT): cv.string})}, extra=vol.ALLOW_EXTRA
|
||||||
vol.Required(CONF_PORT): cv.string,
|
)
|
||||||
}),
|
|
||||||
}, extra=vol.ALLOW_EXTRA)
|
|
||||||
|
|
||||||
|
|
||||||
def setup(hass, config):
|
def setup(hass, config):
|
||||||
|
|
@ -46,8 +43,10 @@ def setup(hass, config):
|
||||||
_LOGGER.error("The StandardFirmata sketch should be 2.2 or newer")
|
_LOGGER.error("The StandardFirmata sketch should be 2.2 or newer")
|
||||||
return False
|
return False
|
||||||
except IndexError:
|
except IndexError:
|
||||||
_LOGGER.warning("The version of the StandardFirmata sketch was not"
|
_LOGGER.warning(
|
||||||
"detected. This may lead to side effects")
|
"The version of the StandardFirmata sketch was not"
|
||||||
|
"detected. This may lead to side effects"
|
||||||
|
)
|
||||||
|
|
||||||
def stop_arduino(event):
|
def stop_arduino(event):
|
||||||
"""Stop the Arduino service."""
|
"""Stop the Arduino service."""
|
||||||
|
|
@ -68,26 +67,22 @@ class ArduinoBoard:
|
||||||
def __init__(self, port):
|
def __init__(self, port):
|
||||||
"""Initialize the board."""
|
"""Initialize the board."""
|
||||||
from PyMata.pymata import PyMata
|
from PyMata.pymata import PyMata
|
||||||
|
|
||||||
self._port = port
|
self._port = port
|
||||||
self._board = PyMata(self._port, verbose=False)
|
self._board = PyMata(self._port, verbose=False)
|
||||||
|
|
||||||
def set_mode(self, pin, direction, mode):
|
def set_mode(self, pin, direction, mode):
|
||||||
"""Set the mode and the direction of a given pin."""
|
"""Set the mode and the direction of a given pin."""
|
||||||
if mode == 'analog' and direction == 'in':
|
if mode == "analog" and direction == "in":
|
||||||
self._board.set_pin_mode(
|
self._board.set_pin_mode(pin, self._board.INPUT, self._board.ANALOG)
|
||||||
pin, self._board.INPUT, self._board.ANALOG)
|
elif mode == "analog" and direction == "out":
|
||||||
elif mode == 'analog' and direction == 'out':
|
self._board.set_pin_mode(pin, self._board.OUTPUT, self._board.ANALOG)
|
||||||
self._board.set_pin_mode(
|
elif mode == "digital" and direction == "in":
|
||||||
pin, self._board.OUTPUT, self._board.ANALOG)
|
self._board.set_pin_mode(pin, self._board.INPUT, self._board.DIGITAL)
|
||||||
elif mode == 'digital' and direction == 'in':
|
elif mode == "digital" and direction == "out":
|
||||||
self._board.set_pin_mode(
|
self._board.set_pin_mode(pin, self._board.OUTPUT, self._board.DIGITAL)
|
||||||
pin, self._board.INPUT, self._board.DIGITAL)
|
elif mode == "pwm":
|
||||||
elif mode == 'digital' and direction == 'out':
|
self._board.set_pin_mode(pin, self._board.OUTPUT, self._board.PWM)
|
||||||
self._board.set_pin_mode(
|
|
||||||
pin, self._board.OUTPUT, self._board.DIGITAL)
|
|
||||||
elif mode == 'pwm':
|
|
||||||
self._board.set_pin_mode(
|
|
||||||
pin, self._board.OUTPUT, self._board.PWM)
|
|
||||||
|
|
||||||
def get_analog_inputs(self):
|
def get_analog_inputs(self):
|
||||||
"""Get the values from the pins."""
|
"""Get the values from the pins."""
|
||||||
|
|
|
||||||
|
|
@ -11,36 +11,39 @@ import voluptuous as vol
|
||||||
from requests.exceptions import HTTPError, ConnectTimeout
|
from requests.exceptions import HTTPError, ConnectTimeout
|
||||||
|
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
from homeassistant.const import (
|
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_SCAN_INTERVAL
|
||||||
CONF_USERNAME, CONF_PASSWORD, CONF_SCAN_INTERVAL)
|
|
||||||
from homeassistant.helpers.event import track_time_interval
|
from homeassistant.helpers.event import track_time_interval
|
||||||
from homeassistant.helpers.dispatcher import dispatcher_send
|
from homeassistant.helpers.dispatcher import dispatcher_send
|
||||||
|
|
||||||
REQUIREMENTS = ['pyarlo==0.2.0']
|
REQUIREMENTS = ["pyarlo==0.2.0"]
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
CONF_ATTRIBUTION = "Data provided by arlo.netgear.com"
|
CONF_ATTRIBUTION = "Data provided by arlo.netgear.com"
|
||||||
|
|
||||||
DATA_ARLO = 'data_arlo'
|
DATA_ARLO = "data_arlo"
|
||||||
DEFAULT_BRAND = 'Netgear Arlo'
|
DEFAULT_BRAND = "Netgear Arlo"
|
||||||
DOMAIN = 'arlo'
|
DOMAIN = "arlo"
|
||||||
|
|
||||||
NOTIFICATION_ID = 'arlo_notification'
|
NOTIFICATION_ID = "arlo_notification"
|
||||||
NOTIFICATION_TITLE = 'Arlo Component Setup'
|
NOTIFICATION_TITLE = "Arlo Component Setup"
|
||||||
|
|
||||||
SCAN_INTERVAL = timedelta(seconds=60)
|
SCAN_INTERVAL = timedelta(seconds=60)
|
||||||
|
|
||||||
SIGNAL_UPDATE_ARLO = "arlo_update"
|
SIGNAL_UPDATE_ARLO = "arlo_update"
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema({
|
CONFIG_SCHEMA = vol.Schema(
|
||||||
DOMAIN: vol.Schema({
|
{
|
||||||
vol.Required(CONF_USERNAME): cv.string,
|
DOMAIN: vol.Schema(
|
||||||
vol.Required(CONF_PASSWORD): cv.string,
|
{
|
||||||
vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL):
|
vol.Required(CONF_USERNAME): cv.string,
|
||||||
cv.time_period,
|
vol.Required(CONF_PASSWORD): cv.string,
|
||||||
}),
|
vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): cv.time_period,
|
||||||
}, extra=vol.ALLOW_EXTRA)
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
extra=vol.ALLOW_EXTRA,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def setup(hass, config):
|
def setup(hass, config):
|
||||||
|
|
@ -58,8 +61,7 @@ def setup(hass, config):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# assign refresh period to base station thread
|
# assign refresh period to base station thread
|
||||||
arlo_base_station = next((
|
arlo_base_station = next((station for station in arlo.base_stations), None)
|
||||||
station for station in arlo.base_stations), None)
|
|
||||||
|
|
||||||
if arlo_base_station is not None:
|
if arlo_base_station is not None:
|
||||||
arlo_base_station.refresh_rate = scan_interval.total_seconds()
|
arlo_base_station.refresh_rate = scan_interval.total_seconds()
|
||||||
|
|
@ -72,22 +74,22 @@ def setup(hass, config):
|
||||||
except (ConnectTimeout, HTTPError) as ex:
|
except (ConnectTimeout, HTTPError) as ex:
|
||||||
_LOGGER.error("Unable to connect to Netgear Arlo: %s", str(ex))
|
_LOGGER.error("Unable to connect to Netgear Arlo: %s", str(ex))
|
||||||
hass.components.persistent_notification.create(
|
hass.components.persistent_notification.create(
|
||||||
'Error: {}<br />'
|
"Error: {}<br />"
|
||||||
'You will need to restart hass after fixing.'
|
"You will need to restart hass after fixing."
|
||||||
''.format(ex),
|
"".format(ex),
|
||||||
title=NOTIFICATION_TITLE,
|
title=NOTIFICATION_TITLE,
|
||||||
notification_id=NOTIFICATION_ID)
|
notification_id=NOTIFICATION_ID,
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def hub_refresh(event_time):
|
def hub_refresh(event_time):
|
||||||
"""Call ArloHub to refresh information."""
|
"""Call ArloHub to refresh information."""
|
||||||
_LOGGER.info("Updating Arlo Hub component")
|
_LOGGER.info("Updating Arlo Hub component")
|
||||||
hass.data[DATA_ARLO].update(update_cameras=True,
|
hass.data[DATA_ARLO].update(update_cameras=True, update_base_station=True)
|
||||||
update_base_station=True)
|
|
||||||
dispatcher_send(hass, SIGNAL_UPDATE_ARLO)
|
dispatcher_send(hass, SIGNAL_UPDATE_ARLO)
|
||||||
|
|
||||||
# register service
|
# register service
|
||||||
hass.services.register(DOMAIN, 'update', hub_refresh)
|
hass.services.register(DOMAIN, "update", hub_refresh)
|
||||||
|
|
||||||
# register scan interval for ArloHub
|
# register scan interval for ArloHub
|
||||||
track_time_interval(hass, hub_refresh, scan_interval)
|
track_time_interval(hass, hub_refresh, scan_interval)
|
||||||
|
|
|
||||||
|
|
@ -13,24 +13,31 @@ from homeassistant.core import callback
|
||||||
from homeassistant.helpers import discovery
|
from homeassistant.helpers import discovery
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.dispatcher import (
|
from homeassistant.helpers.dispatcher import (
|
||||||
async_dispatcher_connect, async_dispatcher_send)
|
async_dispatcher_connect,
|
||||||
|
async_dispatcher_send,
|
||||||
|
)
|
||||||
|
|
||||||
REQUIREMENTS = ['asterisk_mbox==0.5.0']
|
REQUIREMENTS = ["asterisk_mbox==0.5.0"]
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DOMAIN = 'asterisk_mbox'
|
DOMAIN = "asterisk_mbox"
|
||||||
|
|
||||||
SIGNAL_MESSAGE_REQUEST = 'asterisk_mbox.message_request'
|
SIGNAL_MESSAGE_REQUEST = "asterisk_mbox.message_request"
|
||||||
SIGNAL_MESSAGE_UPDATE = 'asterisk_mbox.message_updated'
|
SIGNAL_MESSAGE_UPDATE = "asterisk_mbox.message_updated"
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema({
|
CONFIG_SCHEMA = vol.Schema(
|
||||||
DOMAIN: vol.Schema({
|
{
|
||||||
vol.Required(CONF_HOST): cv.string,
|
DOMAIN: vol.Schema(
|
||||||
vol.Required(CONF_PASSWORD): cv.string,
|
{
|
||||||
vol.Required(CONF_PORT): int,
|
vol.Required(CONF_HOST): cv.string,
|
||||||
}),
|
vol.Required(CONF_PASSWORD): cv.string,
|
||||||
}, extra=vol.ALLOW_EXTRA)
|
vol.Required(CONF_PORT): int,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
extra=vol.ALLOW_EXTRA,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def setup(hass, config):
|
def setup(hass, config):
|
||||||
|
|
@ -43,7 +50,7 @@ def setup(hass, config):
|
||||||
|
|
||||||
hass.data[DOMAIN] = AsteriskData(hass, host, port, password)
|
hass.data[DOMAIN] = AsteriskData(hass, host, port, password)
|
||||||
|
|
||||||
discovery.load_platform(hass, 'mailbox', DOMAIN, {}, config)
|
discovery.load_platform(hass, "mailbox", DOMAIN, {}, config)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
@ -60,7 +67,8 @@ class AsteriskData:
|
||||||
self.messages = []
|
self.messages = []
|
||||||
|
|
||||||
async_dispatcher_connect(
|
async_dispatcher_connect(
|
||||||
self.hass, SIGNAL_MESSAGE_REQUEST, self._request_messages)
|
self.hass, SIGNAL_MESSAGE_REQUEST, self._request_messages
|
||||||
|
)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def handle_data(self, command, msg):
|
def handle_data(self, command, msg):
|
||||||
|
|
@ -70,9 +78,9 @@ class AsteriskData:
|
||||||
if command == CMD_MESSAGE_LIST:
|
if command == CMD_MESSAGE_LIST:
|
||||||
_LOGGER.debug("AsteriskVM sent updated message list")
|
_LOGGER.debug("AsteriskVM sent updated message list")
|
||||||
self.messages = sorted(
|
self.messages = sorted(
|
||||||
msg, key=lambda item: item['info']['origtime'], reverse=True)
|
msg, key=lambda item: item["info"]["origtime"], reverse=True
|
||||||
async_dispatcher_send(
|
)
|
||||||
self.hass, SIGNAL_MESSAGE_UPDATE, self.messages)
|
async_dispatcher_send(self.hass, SIGNAL_MESSAGE_UPDATE, self.messages)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _request_messages(self):
|
def _request_messages(self):
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,7 @@ import voluptuous as vol
|
||||||
from requests import RequestException
|
from requests import RequestException
|
||||||
|
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.const import (
|
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, CONF_TIMEOUT
|
||||||
CONF_PASSWORD, CONF_USERNAME, CONF_TIMEOUT)
|
|
||||||
from homeassistant.helpers import discovery
|
from homeassistant.helpers import discovery
|
||||||
from homeassistant.util import Throttle
|
from homeassistant.util import Throttle
|
||||||
|
|
||||||
|
|
@ -21,40 +20,43 @@ _LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
_CONFIGURING = {}
|
_CONFIGURING = {}
|
||||||
|
|
||||||
REQUIREMENTS = ['py-august==0.6.0']
|
REQUIREMENTS = ["py-august==0.6.0"]
|
||||||
|
|
||||||
DEFAULT_TIMEOUT = 10
|
DEFAULT_TIMEOUT = 10
|
||||||
ACTIVITY_FETCH_LIMIT = 10
|
ACTIVITY_FETCH_LIMIT = 10
|
||||||
ACTIVITY_INITIAL_FETCH_LIMIT = 20
|
ACTIVITY_INITIAL_FETCH_LIMIT = 20
|
||||||
|
|
||||||
CONF_LOGIN_METHOD = 'login_method'
|
CONF_LOGIN_METHOD = "login_method"
|
||||||
CONF_INSTALL_ID = 'install_id'
|
CONF_INSTALL_ID = "install_id"
|
||||||
|
|
||||||
NOTIFICATION_ID = 'august_notification'
|
NOTIFICATION_ID = "august_notification"
|
||||||
NOTIFICATION_TITLE = "August Setup"
|
NOTIFICATION_TITLE = "August Setup"
|
||||||
|
|
||||||
AUGUST_CONFIG_FILE = '.august.conf'
|
AUGUST_CONFIG_FILE = ".august.conf"
|
||||||
|
|
||||||
DATA_AUGUST = 'august'
|
DATA_AUGUST = "august"
|
||||||
DOMAIN = 'august'
|
DOMAIN = "august"
|
||||||
DEFAULT_ENTITY_NAMESPACE = 'august'
|
DEFAULT_ENTITY_NAMESPACE = "august"
|
||||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5)
|
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5)
|
||||||
DEFAULT_SCAN_INTERVAL = timedelta(seconds=5)
|
DEFAULT_SCAN_INTERVAL = timedelta(seconds=5)
|
||||||
LOGIN_METHODS = ['phone', 'email']
|
LOGIN_METHODS = ["phone", "email"]
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema({
|
CONFIG_SCHEMA = vol.Schema(
|
||||||
DOMAIN: vol.Schema({
|
{
|
||||||
vol.Required(CONF_LOGIN_METHOD): vol.In(LOGIN_METHODS),
|
DOMAIN: vol.Schema(
|
||||||
vol.Required(CONF_USERNAME): cv.string,
|
{
|
||||||
vol.Required(CONF_PASSWORD): cv.string,
|
vol.Required(CONF_LOGIN_METHOD): vol.In(LOGIN_METHODS),
|
||||||
vol.Optional(CONF_INSTALL_ID): cv.string,
|
vol.Required(CONF_USERNAME): cv.string,
|
||||||
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
|
vol.Required(CONF_PASSWORD): cv.string,
|
||||||
})
|
vol.Optional(CONF_INSTALL_ID): cv.string,
|
||||||
}, extra=vol.ALLOW_EXTRA)
|
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
extra=vol.ALLOW_EXTRA,
|
||||||
|
)
|
||||||
|
|
||||||
AUGUST_COMPONENTS = [
|
AUGUST_COMPONENTS = ["camera", "binary_sensor", "lock"]
|
||||||
'camera', 'binary_sensor', 'lock'
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def request_configuration(hass, config, api, authenticator):
|
def request_configuration(hass, config, api, authenticator):
|
||||||
|
|
@ -65,12 +67,12 @@ def request_configuration(hass, config, api, authenticator):
|
||||||
"""Run when the configuration callback is called."""
|
"""Run when the configuration callback is called."""
|
||||||
from august.authenticator import ValidationResult
|
from august.authenticator import ValidationResult
|
||||||
|
|
||||||
result = authenticator.validate_verification_code(
|
result = authenticator.validate_verification_code(data.get("verification_code"))
|
||||||
data.get('verification_code'))
|
|
||||||
|
|
||||||
if result == ValidationResult.INVALID_VERIFICATION_CODE:
|
if result == ValidationResult.INVALID_VERIFICATION_CODE:
|
||||||
configurator.notify_errors(_CONFIGURING[DOMAIN],
|
configurator.notify_errors(
|
||||||
"Invalid verification code")
|
_CONFIGURING[DOMAIN], "Invalid verification code"
|
||||||
|
)
|
||||||
elif result == ValidationResult.VALIDATED:
|
elif result == ValidationResult.VALIDATED:
|
||||||
setup_august(hass, config, api, authenticator)
|
setup_august(hass, config, api, authenticator)
|
||||||
|
|
||||||
|
|
@ -85,12 +87,11 @@ def request_configuration(hass, config, api, authenticator):
|
||||||
NOTIFICATION_TITLE,
|
NOTIFICATION_TITLE,
|
||||||
august_configuration_callback,
|
august_configuration_callback,
|
||||||
description="Please check your {} ({}) and enter the verification "
|
description="Please check your {} ({}) and enter the verification "
|
||||||
"code below".format(login_method, username),
|
"code below".format(login_method, username),
|
||||||
submit_caption='Verify',
|
submit_caption="Verify",
|
||||||
fields=[{
|
fields=[
|
||||||
'id': 'verification_code',
|
{"id": "verification_code", "name": "Verification code", "type": "string"}
|
||||||
'name': "Verification code",
|
],
|
||||||
'type': 'string'}]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -109,7 +110,8 @@ def setup_august(hass, config, api, authenticator):
|
||||||
"You will need to restart hass after fixing."
|
"You will need to restart hass after fixing."
|
||||||
"".format(ex),
|
"".format(ex),
|
||||||
title=NOTIFICATION_TITLE,
|
title=NOTIFICATION_TITLE,
|
||||||
notification_id=NOTIFICATION_ID)
|
notification_id=NOTIFICATION_ID,
|
||||||
|
)
|
||||||
|
|
||||||
state = authentication.state
|
state = authentication.state
|
||||||
|
|
||||||
|
|
@ -146,7 +148,8 @@ def setup(hass, config):
|
||||||
conf.get(CONF_USERNAME),
|
conf.get(CONF_USERNAME),
|
||||||
conf.get(CONF_PASSWORD),
|
conf.get(CONF_PASSWORD),
|
||||||
install_id=conf.get(CONF_INSTALL_ID),
|
install_id=conf.get(CONF_INSTALL_ID),
|
||||||
access_token_cache_file=hass.config.path(AUGUST_CONFIG_FILE))
|
access_token_cache_file=hass.config.path(AUGUST_CONFIG_FILE),
|
||||||
|
)
|
||||||
|
|
||||||
return setup_august(hass, config, api, authenticator)
|
return setup_august(hass, config, api, authenticator)
|
||||||
|
|
||||||
|
|
@ -200,14 +203,15 @@ class AugustData:
|
||||||
def _update_device_activities(self, limit=ACTIVITY_FETCH_LIMIT):
|
def _update_device_activities(self, limit=ACTIVITY_FETCH_LIMIT):
|
||||||
"""Update data object with latest from August API."""
|
"""Update data object with latest from August API."""
|
||||||
for house_id in self.house_ids:
|
for house_id in self.house_ids:
|
||||||
activities = self._api.get_house_activities(self._access_token,
|
activities = self._api.get_house_activities(
|
||||||
house_id,
|
self._access_token, house_id, limit=limit
|
||||||
limit=limit)
|
)
|
||||||
|
|
||||||
device_ids = {a.device_id for a in activities}
|
device_ids = {a.device_id for a in activities}
|
||||||
for device_id in device_ids:
|
for device_id in device_ids:
|
||||||
self._activities_by_id[device_id] = [a for a in activities if
|
self._activities_by_id[device_id] = [
|
||||||
a.device_id == device_id]
|
a for a in activities if a.device_id == device_id
|
||||||
|
]
|
||||||
|
|
||||||
def get_doorbell_detail(self, doorbell_id):
|
def get_doorbell_detail(self, doorbell_id):
|
||||||
"""Return doorbell detail."""
|
"""Return doorbell detail."""
|
||||||
|
|
@ -220,7 +224,8 @@ class AugustData:
|
||||||
|
|
||||||
for doorbell in self._doorbells:
|
for doorbell in self._doorbells:
|
||||||
detail_by_id[doorbell.device_id] = self._api.get_doorbell_detail(
|
detail_by_id[doorbell.device_id] = self._api.get_doorbell_detail(
|
||||||
self._access_token, doorbell.device_id)
|
self._access_token, doorbell.device_id
|
||||||
|
)
|
||||||
|
|
||||||
self._doorbell_detail_by_id = detail_by_id
|
self._doorbell_detail_by_id = detail_by_id
|
||||||
|
|
||||||
|
|
@ -241,9 +246,11 @@ class AugustData:
|
||||||
|
|
||||||
for lock in self._locks:
|
for lock in self._locks:
|
||||||
status_by_id[lock.device_id] = self._api.get_lock_status(
|
status_by_id[lock.device_id] = self._api.get_lock_status(
|
||||||
self._access_token, lock.device_id)
|
self._access_token, lock.device_id
|
||||||
|
)
|
||||||
detail_by_id[lock.device_id] = self._api.get_lock_detail(
|
detail_by_id[lock.device_id] = self._api.get_lock_detail(
|
||||||
self._access_token, lock.device_id)
|
self._access_token, lock.device_id
|
||||||
|
)
|
||||||
|
|
||||||
self._lock_status_by_id = status_by_id
|
self._lock_status_by_id = status_by_id
|
||||||
self._lock_detail_by_id = detail_by_id
|
self._lock_detail_by_id = detail_by_id
|
||||||
|
|
|
||||||
|
|
@ -126,8 +126,11 @@ from datetime import timedelta
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.auth.models import User, Credentials, \
|
from homeassistant.auth.models import (
|
||||||
TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN
|
User,
|
||||||
|
Credentials,
|
||||||
|
TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN,
|
||||||
|
)
|
||||||
from homeassistant.components import websocket_api
|
from homeassistant.components import websocket_api
|
||||||
from homeassistant.components.http import KEY_REAL_IP
|
from homeassistant.components.http import KEY_REAL_IP
|
||||||
from homeassistant.components.http.ban import log_invalid_auth
|
from homeassistant.components.http.ban import log_invalid_auth
|
||||||
|
|
@ -140,38 +143,39 @@ from . import indieauth
|
||||||
from . import login_flow
|
from . import login_flow
|
||||||
from . import mfa_setup_flow
|
from . import mfa_setup_flow
|
||||||
|
|
||||||
DOMAIN = 'auth'
|
DOMAIN = "auth"
|
||||||
DEPENDENCIES = ['http']
|
DEPENDENCIES = ["http"]
|
||||||
|
|
||||||
WS_TYPE_CURRENT_USER = 'auth/current_user'
|
WS_TYPE_CURRENT_USER = "auth/current_user"
|
||||||
SCHEMA_WS_CURRENT_USER = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
SCHEMA_WS_CURRENT_USER = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
|
||||||
vol.Required('type'): WS_TYPE_CURRENT_USER,
|
{vol.Required("type"): WS_TYPE_CURRENT_USER}
|
||||||
})
|
)
|
||||||
|
|
||||||
WS_TYPE_LONG_LIVED_ACCESS_TOKEN = 'auth/long_lived_access_token'
|
WS_TYPE_LONG_LIVED_ACCESS_TOKEN = "auth/long_lived_access_token"
|
||||||
SCHEMA_WS_LONG_LIVED_ACCESS_TOKEN = \
|
SCHEMA_WS_LONG_LIVED_ACCESS_TOKEN = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
|
||||||
websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
{
|
||||||
vol.Required('type'): WS_TYPE_LONG_LIVED_ACCESS_TOKEN,
|
vol.Required("type"): WS_TYPE_LONG_LIVED_ACCESS_TOKEN,
|
||||||
vol.Required('lifespan'): int, # days
|
vol.Required("lifespan"): int, # days
|
||||||
vol.Required('client_name'): str,
|
vol.Required("client_name"): str,
|
||||||
vol.Optional('client_icon'): str,
|
vol.Optional("client_icon"): str,
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
WS_TYPE_REFRESH_TOKENS = 'auth/refresh_tokens'
|
WS_TYPE_REFRESH_TOKENS = "auth/refresh_tokens"
|
||||||
SCHEMA_WS_REFRESH_TOKENS = \
|
SCHEMA_WS_REFRESH_TOKENS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
|
||||||
websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
{vol.Required("type"): WS_TYPE_REFRESH_TOKENS}
|
||||||
vol.Required('type'): WS_TYPE_REFRESH_TOKENS,
|
)
|
||||||
})
|
|
||||||
|
|
||||||
WS_TYPE_DELETE_REFRESH_TOKEN = 'auth/delete_refresh_token'
|
WS_TYPE_DELETE_REFRESH_TOKEN = "auth/delete_refresh_token"
|
||||||
SCHEMA_WS_DELETE_REFRESH_TOKEN = \
|
SCHEMA_WS_DELETE_REFRESH_TOKEN = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
|
||||||
websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
{
|
||||||
vol.Required('type'): WS_TYPE_DELETE_REFRESH_TOKEN,
|
vol.Required("type"): WS_TYPE_DELETE_REFRESH_TOKEN,
|
||||||
vol.Required('refresh_token_id'): str,
|
vol.Required("refresh_token_id"): str,
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
RESULT_TYPE_CREDENTIALS = 'credentials'
|
RESULT_TYPE_CREDENTIALS = "credentials"
|
||||||
RESULT_TYPE_USER = 'user'
|
RESULT_TYPE_USER = "user"
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -184,23 +188,20 @@ async def async_setup(hass, config):
|
||||||
hass.http.register_view(LinkUserView(retrieve_result))
|
hass.http.register_view(LinkUserView(retrieve_result))
|
||||||
|
|
||||||
hass.components.websocket_api.async_register_command(
|
hass.components.websocket_api.async_register_command(
|
||||||
WS_TYPE_CURRENT_USER, websocket_current_user,
|
WS_TYPE_CURRENT_USER, websocket_current_user, SCHEMA_WS_CURRENT_USER
|
||||||
SCHEMA_WS_CURRENT_USER
|
|
||||||
)
|
)
|
||||||
hass.components.websocket_api.async_register_command(
|
hass.components.websocket_api.async_register_command(
|
||||||
WS_TYPE_LONG_LIVED_ACCESS_TOKEN,
|
WS_TYPE_LONG_LIVED_ACCESS_TOKEN,
|
||||||
websocket_create_long_lived_access_token,
|
websocket_create_long_lived_access_token,
|
||||||
SCHEMA_WS_LONG_LIVED_ACCESS_TOKEN
|
SCHEMA_WS_LONG_LIVED_ACCESS_TOKEN,
|
||||||
)
|
)
|
||||||
hass.components.websocket_api.async_register_command(
|
hass.components.websocket_api.async_register_command(
|
||||||
WS_TYPE_REFRESH_TOKENS,
|
WS_TYPE_REFRESH_TOKENS, websocket_refresh_tokens, SCHEMA_WS_REFRESH_TOKENS
|
||||||
websocket_refresh_tokens,
|
|
||||||
SCHEMA_WS_REFRESH_TOKENS
|
|
||||||
)
|
)
|
||||||
hass.components.websocket_api.async_register_command(
|
hass.components.websocket_api.async_register_command(
|
||||||
WS_TYPE_DELETE_REFRESH_TOKEN,
|
WS_TYPE_DELETE_REFRESH_TOKEN,
|
||||||
websocket_delete_refresh_token,
|
websocket_delete_refresh_token,
|
||||||
SCHEMA_WS_DELETE_REFRESH_TOKEN
|
SCHEMA_WS_DELETE_REFRESH_TOKEN,
|
||||||
)
|
)
|
||||||
|
|
||||||
await login_flow.async_setup(hass, store_result)
|
await login_flow.async_setup(hass, store_result)
|
||||||
|
|
@ -212,8 +213,8 @@ async def async_setup(hass, config):
|
||||||
class TokenView(HomeAssistantView):
|
class TokenView(HomeAssistantView):
|
||||||
"""View to issue or revoke tokens."""
|
"""View to issue or revoke tokens."""
|
||||||
|
|
||||||
url = '/auth/token'
|
url = "/auth/token"
|
||||||
name = 'api:auth:token'
|
name = "api:auth:token"
|
||||||
requires_auth = False
|
requires_auth = False
|
||||||
cors_allowed = True
|
cors_allowed = True
|
||||||
|
|
||||||
|
|
@ -224,29 +225,29 @@ class TokenView(HomeAssistantView):
|
||||||
@log_invalid_auth
|
@log_invalid_auth
|
||||||
async def post(self, request):
|
async def post(self, request):
|
||||||
"""Grant a token."""
|
"""Grant a token."""
|
||||||
hass = request.app['hass']
|
hass = request.app["hass"]
|
||||||
data = await request.post()
|
data = await request.post()
|
||||||
|
|
||||||
grant_type = data.get('grant_type')
|
grant_type = data.get("grant_type")
|
||||||
|
|
||||||
# IndieAuth 6.3.5
|
# IndieAuth 6.3.5
|
||||||
# The revocation endpoint is the same as the token endpoint.
|
# The revocation endpoint is the same as the token endpoint.
|
||||||
# The revocation request includes an additional parameter,
|
# The revocation request includes an additional parameter,
|
||||||
# action=revoke.
|
# action=revoke.
|
||||||
if data.get('action') == 'revoke':
|
if data.get("action") == "revoke":
|
||||||
return await self._async_handle_revoke_token(hass, data)
|
return await self._async_handle_revoke_token(hass, data)
|
||||||
|
|
||||||
if grant_type == 'authorization_code':
|
if grant_type == "authorization_code":
|
||||||
return await self._async_handle_auth_code(
|
return await self._async_handle_auth_code(
|
||||||
hass, data, str(request[KEY_REAL_IP]))
|
hass, data, str(request[KEY_REAL_IP])
|
||||||
|
)
|
||||||
|
|
||||||
if grant_type == 'refresh_token':
|
if grant_type == "refresh_token":
|
||||||
return await self._async_handle_refresh_token(
|
return await self._async_handle_refresh_token(
|
||||||
hass, data, str(request[KEY_REAL_IP]))
|
hass, data, str(request[KEY_REAL_IP])
|
||||||
|
)
|
||||||
|
|
||||||
return self.json({
|
return self.json({"error": "unsupported_grant_type"}, status_code=400)
|
||||||
'error': 'unsupported_grant_type',
|
|
||||||
}, status_code=400)
|
|
||||||
|
|
||||||
async def _async_handle_revoke_token(self, hass, data):
|
async def _async_handle_revoke_token(self, hass, data):
|
||||||
"""Handle revoke token request."""
|
"""Handle revoke token request."""
|
||||||
|
|
@ -254,7 +255,7 @@ class TokenView(HomeAssistantView):
|
||||||
# 2.2 The authorization server responds with HTTP status code 200
|
# 2.2 The authorization server responds with HTTP status code 200
|
||||||
# if the token has been revoked successfully or if the client
|
# if the token has been revoked successfully or if the client
|
||||||
# submitted an invalid token.
|
# submitted an invalid token.
|
||||||
token = data.get('token')
|
token = data.get("token")
|
||||||
|
|
||||||
if token is None:
|
if token is None:
|
||||||
return web.Response(status=200)
|
return web.Response(status=200)
|
||||||
|
|
@ -269,117 +270,112 @@ class TokenView(HomeAssistantView):
|
||||||
|
|
||||||
async def _async_handle_auth_code(self, hass, data, remote_addr):
|
async def _async_handle_auth_code(self, hass, data, remote_addr):
|
||||||
"""Handle authorization code request."""
|
"""Handle authorization code request."""
|
||||||
client_id = data.get('client_id')
|
client_id = data.get("client_id")
|
||||||
if client_id is None or not indieauth.verify_client_id(client_id):
|
if client_id is None or not indieauth.verify_client_id(client_id):
|
||||||
return self.json({
|
return self.json(
|
||||||
'error': 'invalid_request',
|
{"error": "invalid_request", "error_description": "Invalid client id"},
|
||||||
'error_description': 'Invalid client id',
|
status_code=400,
|
||||||
}, status_code=400)
|
)
|
||||||
|
|
||||||
code = data.get('code')
|
code = data.get("code")
|
||||||
|
|
||||||
if code is None:
|
if code is None:
|
||||||
return self.json({
|
return self.json(
|
||||||
'error': 'invalid_request',
|
{"error": "invalid_request", "error_description": "Invalid code"},
|
||||||
'error_description': 'Invalid code',
|
status_code=400,
|
||||||
}, status_code=400)
|
)
|
||||||
|
|
||||||
user = self._retrieve_user(client_id, RESULT_TYPE_USER, code)
|
user = self._retrieve_user(client_id, RESULT_TYPE_USER, code)
|
||||||
|
|
||||||
if user is None or not isinstance(user, User):
|
if user is None or not isinstance(user, User):
|
||||||
return self.json({
|
return self.json(
|
||||||
'error': 'invalid_request',
|
{"error": "invalid_request", "error_description": "Invalid code"},
|
||||||
'error_description': 'Invalid code',
|
status_code=400,
|
||||||
}, status_code=400)
|
)
|
||||||
|
|
||||||
# refresh user
|
# refresh user
|
||||||
user = await hass.auth.async_get_user(user.id)
|
user = await hass.auth.async_get_user(user.id)
|
||||||
|
|
||||||
if not user.is_active:
|
if not user.is_active:
|
||||||
return self.json({
|
return self.json(
|
||||||
'error': 'access_denied',
|
{"error": "access_denied", "error_description": "User is not active"},
|
||||||
'error_description': 'User is not active',
|
status_code=403,
|
||||||
}, status_code=403)
|
)
|
||||||
|
|
||||||
refresh_token = await hass.auth.async_create_refresh_token(user,
|
refresh_token = await hass.auth.async_create_refresh_token(user, client_id)
|
||||||
client_id)
|
access_token = hass.auth.async_create_access_token(refresh_token, remote_addr)
|
||||||
access_token = hass.auth.async_create_access_token(
|
|
||||||
refresh_token, remote_addr)
|
|
||||||
|
|
||||||
return self.json({
|
return self.json(
|
||||||
'access_token': access_token,
|
{
|
||||||
'token_type': 'Bearer',
|
"access_token": access_token,
|
||||||
'refresh_token': refresh_token.token,
|
"token_type": "Bearer",
|
||||||
'expires_in':
|
"refresh_token": refresh_token.token,
|
||||||
int(refresh_token.access_token_expiration.total_seconds()),
|
"expires_in": int(
|
||||||
})
|
refresh_token.access_token_expiration.total_seconds()
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
async def _async_handle_refresh_token(self, hass, data, remote_addr):
|
async def _async_handle_refresh_token(self, hass, data, remote_addr):
|
||||||
"""Handle authorization code request."""
|
"""Handle authorization code request."""
|
||||||
client_id = data.get('client_id')
|
client_id = data.get("client_id")
|
||||||
if client_id is not None and not indieauth.verify_client_id(client_id):
|
if client_id is not None and not indieauth.verify_client_id(client_id):
|
||||||
return self.json({
|
return self.json(
|
||||||
'error': 'invalid_request',
|
{"error": "invalid_request", "error_description": "Invalid client id"},
|
||||||
'error_description': 'Invalid client id',
|
status_code=400,
|
||||||
}, status_code=400)
|
)
|
||||||
|
|
||||||
token = data.get('refresh_token')
|
token = data.get("refresh_token")
|
||||||
|
|
||||||
if token is None:
|
if token is None:
|
||||||
return self.json({
|
return self.json({"error": "invalid_request"}, status_code=400)
|
||||||
'error': 'invalid_request',
|
|
||||||
}, status_code=400)
|
|
||||||
|
|
||||||
refresh_token = await hass.auth.async_get_refresh_token_by_token(token)
|
refresh_token = await hass.auth.async_get_refresh_token_by_token(token)
|
||||||
|
|
||||||
if refresh_token is None:
|
if refresh_token is None:
|
||||||
return self.json({
|
return self.json({"error": "invalid_grant"}, status_code=400)
|
||||||
'error': 'invalid_grant',
|
|
||||||
}, status_code=400)
|
|
||||||
|
|
||||||
if refresh_token.client_id != client_id:
|
if refresh_token.client_id != client_id:
|
||||||
return self.json({
|
return self.json({"error": "invalid_request"}, status_code=400)
|
||||||
'error': 'invalid_request',
|
|
||||||
}, status_code=400)
|
|
||||||
|
|
||||||
access_token = hass.auth.async_create_access_token(
|
access_token = hass.auth.async_create_access_token(refresh_token, remote_addr)
|
||||||
refresh_token, remote_addr)
|
|
||||||
|
|
||||||
return self.json({
|
return self.json(
|
||||||
'access_token': access_token,
|
{
|
||||||
'token_type': 'Bearer',
|
"access_token": access_token,
|
||||||
'expires_in':
|
"token_type": "Bearer",
|
||||||
int(refresh_token.access_token_expiration.total_seconds()),
|
"expires_in": int(
|
||||||
})
|
refresh_token.access_token_expiration.total_seconds()
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class LinkUserView(HomeAssistantView):
|
class LinkUserView(HomeAssistantView):
|
||||||
"""View to link existing users to new credentials."""
|
"""View to link existing users to new credentials."""
|
||||||
|
|
||||||
url = '/auth/link_user'
|
url = "/auth/link_user"
|
||||||
name = 'api:auth:link_user'
|
name = "api:auth:link_user"
|
||||||
|
|
||||||
def __init__(self, retrieve_credentials):
|
def __init__(self, retrieve_credentials):
|
||||||
"""Initialize the link user view."""
|
"""Initialize the link user view."""
|
||||||
self._retrieve_credentials = retrieve_credentials
|
self._retrieve_credentials = retrieve_credentials
|
||||||
|
|
||||||
@RequestDataValidator(vol.Schema({
|
@RequestDataValidator(vol.Schema({"code": str, "client_id": str}))
|
||||||
'code': str,
|
|
||||||
'client_id': str,
|
|
||||||
}))
|
|
||||||
async def post(self, request, data):
|
async def post(self, request, data):
|
||||||
"""Link a user."""
|
"""Link a user."""
|
||||||
hass = request.app['hass']
|
hass = request.app["hass"]
|
||||||
user = request['hass_user']
|
user = request["hass_user"]
|
||||||
|
|
||||||
credentials = self._retrieve_credentials(
|
credentials = self._retrieve_credentials(
|
||||||
data['client_id'], RESULT_TYPE_CREDENTIALS, data['code'])
|
data["client_id"], RESULT_TYPE_CREDENTIALS, data["code"]
|
||||||
|
)
|
||||||
|
|
||||||
if credentials is None:
|
if credentials is None:
|
||||||
return self.json_message('Invalid code', status_code=400)
|
return self.json_message("Invalid code", status_code=400)
|
||||||
|
|
||||||
await hass.auth.async_link_user(user, credentials)
|
await hass.auth.async_link_user(user, credentials)
|
||||||
return self.json_message('User linked')
|
return self.json_message("User linked")
|
||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
|
|
@ -395,11 +391,14 @@ def _create_auth_code_store():
|
||||||
elif isinstance(result, Credentials):
|
elif isinstance(result, Credentials):
|
||||||
result_type = RESULT_TYPE_CREDENTIALS
|
result_type = RESULT_TYPE_CREDENTIALS
|
||||||
else:
|
else:
|
||||||
raise ValueError('result has to be either User or Credentials')
|
raise ValueError("result has to be either User or Credentials")
|
||||||
|
|
||||||
code = uuid.uuid4().hex
|
code = uuid.uuid4().hex
|
||||||
temp_results[(client_id, result_type, code)] = \
|
temp_results[(client_id, result_type, code)] = (
|
||||||
(dt_util.utcnow(), result_type, result)
|
dt_util.utcnow(),
|
||||||
|
result_type,
|
||||||
|
result,
|
||||||
|
)
|
||||||
return code
|
return code
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
|
|
@ -427,26 +426,39 @@ def _create_auth_code_store():
|
||||||
@websocket_api.ws_require_user()
|
@websocket_api.ws_require_user()
|
||||||
@callback
|
@callback
|
||||||
def websocket_current_user(
|
def websocket_current_user(
|
||||||
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg):
|
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg
|
||||||
|
):
|
||||||
"""Return the current user."""
|
"""Return the current user."""
|
||||||
|
|
||||||
async def async_get_current_user(user):
|
async def async_get_current_user(user):
|
||||||
"""Get current user."""
|
"""Get current user."""
|
||||||
enabled_modules = await hass.auth.async_get_enabled_mfa(user)
|
enabled_modules = await hass.auth.async_get_enabled_mfa(user)
|
||||||
|
|
||||||
connection.send_message_outside(
|
connection.send_message_outside(
|
||||||
websocket_api.result_message(msg['id'], {
|
websocket_api.result_message(
|
||||||
'id': user.id,
|
msg["id"],
|
||||||
'name': user.name,
|
{
|
||||||
'is_owner': user.is_owner,
|
"id": user.id,
|
||||||
'credentials': [{'auth_provider_type': c.auth_provider_type,
|
"name": user.name,
|
||||||
'auth_provider_id': c.auth_provider_id}
|
"is_owner": user.is_owner,
|
||||||
for c in user.credentials],
|
"credentials": [
|
||||||
'mfa_modules': [{
|
{
|
||||||
'id': module.id,
|
"auth_provider_type": c.auth_provider_type,
|
||||||
'name': module.name,
|
"auth_provider_id": c.auth_provider_id,
|
||||||
'enabled': module.id in enabled_modules,
|
}
|
||||||
} for module in hass.auth.auth_mfa_modules],
|
for c in user.credentials
|
||||||
}))
|
],
|
||||||
|
"mfa_modules": [
|
||||||
|
{
|
||||||
|
"id": module.id,
|
||||||
|
"name": module.name,
|
||||||
|
"enabled": module.id in enabled_modules,
|
||||||
|
}
|
||||||
|
for module in hass.auth.auth_mfa_modules
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
hass.async_create_task(async_get_current_user(connection.user))
|
hass.async_create_task(async_get_current_user(connection.user))
|
||||||
|
|
||||||
|
|
@ -454,63 +466,77 @@ def websocket_current_user(
|
||||||
@websocket_api.ws_require_user()
|
@websocket_api.ws_require_user()
|
||||||
@callback
|
@callback
|
||||||
def websocket_create_long_lived_access_token(
|
def websocket_create_long_lived_access_token(
|
||||||
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg):
|
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg
|
||||||
|
):
|
||||||
"""Create or a long-lived access token."""
|
"""Create or a long-lived access token."""
|
||||||
|
|
||||||
async def async_create_long_lived_access_token(user):
|
async def async_create_long_lived_access_token(user):
|
||||||
"""Create or a long-lived access token."""
|
"""Create or a long-lived access token."""
|
||||||
refresh_token = await hass.auth.async_create_refresh_token(
|
refresh_token = await hass.auth.async_create_refresh_token(
|
||||||
user,
|
user,
|
||||||
client_name=msg['client_name'],
|
client_name=msg["client_name"],
|
||||||
client_icon=msg.get('client_icon'),
|
client_icon=msg.get("client_icon"),
|
||||||
token_type=TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN,
|
token_type=TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN,
|
||||||
access_token_expiration=timedelta(days=msg['lifespan']))
|
access_token_expiration=timedelta(days=msg["lifespan"]),
|
||||||
|
)
|
||||||
|
|
||||||
access_token = hass.auth.async_create_access_token(
|
access_token = hass.auth.async_create_access_token(refresh_token)
|
||||||
refresh_token)
|
|
||||||
|
|
||||||
connection.send_message_outside(
|
connection.send_message_outside(
|
||||||
websocket_api.result_message(msg['id'], access_token))
|
websocket_api.result_message(msg["id"], access_token)
|
||||||
|
)
|
||||||
|
|
||||||
hass.async_create_task(
|
hass.async_create_task(async_create_long_lived_access_token(connection.user))
|
||||||
async_create_long_lived_access_token(connection.user))
|
|
||||||
|
|
||||||
|
|
||||||
@websocket_api.ws_require_user()
|
@websocket_api.ws_require_user()
|
||||||
@callback
|
@callback
|
||||||
def websocket_refresh_tokens(
|
def websocket_refresh_tokens(
|
||||||
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg):
|
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg
|
||||||
|
):
|
||||||
"""Return metadata of users refresh tokens."""
|
"""Return metadata of users refresh tokens."""
|
||||||
current_id = connection.request.get('refresh_token_id')
|
current_id = connection.request.get("refresh_token_id")
|
||||||
connection.to_write.put_nowait(websocket_api.result_message(msg['id'], [{
|
connection.to_write.put_nowait(
|
||||||
'id': refresh.id,
|
websocket_api.result_message(
|
||||||
'client_id': refresh.client_id,
|
msg["id"],
|
||||||
'client_name': refresh.client_name,
|
[
|
||||||
'client_icon': refresh.client_icon,
|
{
|
||||||
'type': refresh.token_type,
|
"id": refresh.id,
|
||||||
'created_at': refresh.created_at,
|
"client_id": refresh.client_id,
|
||||||
'is_current': refresh.id == current_id,
|
"client_name": refresh.client_name,
|
||||||
'last_used_at': refresh.last_used_at,
|
"client_icon": refresh.client_icon,
|
||||||
'last_used_ip': refresh.last_used_ip,
|
"type": refresh.token_type,
|
||||||
} for refresh in connection.user.refresh_tokens.values()]))
|
"created_at": refresh.created_at,
|
||||||
|
"is_current": refresh.id == current_id,
|
||||||
|
"last_used_at": refresh.last_used_at,
|
||||||
|
"last_used_ip": refresh.last_used_ip,
|
||||||
|
}
|
||||||
|
for refresh in connection.user.refresh_tokens.values()
|
||||||
|
],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@websocket_api.ws_require_user()
|
@websocket_api.ws_require_user()
|
||||||
@callback
|
@callback
|
||||||
def websocket_delete_refresh_token(
|
def websocket_delete_refresh_token(
|
||||||
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg):
|
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg
|
||||||
|
):
|
||||||
"""Handle a delete refresh token request."""
|
"""Handle a delete refresh token request."""
|
||||||
|
|
||||||
async def async_delete_refresh_token(user, refresh_token_id):
|
async def async_delete_refresh_token(user, refresh_token_id):
|
||||||
"""Delete a refresh token."""
|
"""Delete a refresh token."""
|
||||||
refresh_token = connection.user.refresh_tokens.get(refresh_token_id)
|
refresh_token = connection.user.refresh_tokens.get(refresh_token_id)
|
||||||
|
|
||||||
if refresh_token is None:
|
if refresh_token is None:
|
||||||
return websocket_api.error_message(
|
return websocket_api.error_message(
|
||||||
msg['id'], 'invalid_token_id', 'Received invalid token')
|
msg["id"], "invalid_token_id", "Received invalid token"
|
||||||
|
)
|
||||||
|
|
||||||
await hass.auth.async_remove_refresh_token(refresh_token)
|
await hass.auth.async_remove_refresh_token(refresh_token)
|
||||||
|
|
||||||
connection.send_message_outside(
|
connection.send_message_outside(websocket_api.result_message(msg["id"], {}))
|
||||||
websocket_api.result_message(msg['id'], {}))
|
|
||||||
|
|
||||||
hass.async_create_task(
|
hass.async_create_task(
|
||||||
async_delete_refresh_token(connection.user, msg['refresh_token_id']))
|
async_delete_refresh_token(connection.user, msg["refresh_token_id"])
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -8,16 +8,13 @@ import aiohttp
|
||||||
from aiohttp.client_exceptions import ClientError
|
from aiohttp.client_exceptions import ClientError
|
||||||
|
|
||||||
# IP addresses of loopback interfaces
|
# IP addresses of loopback interfaces
|
||||||
ALLOWED_IPS = (
|
ALLOWED_IPS = (ip_address("127.0.0.1"), ip_address("::1"))
|
||||||
ip_address('127.0.0.1'),
|
|
||||||
ip_address('::1'),
|
|
||||||
)
|
|
||||||
|
|
||||||
# RFC1918 - Address allocation for Private Internets
|
# RFC1918 - Address allocation for Private Internets
|
||||||
ALLOWED_NETWORKS = (
|
ALLOWED_NETWORKS = (
|
||||||
ip_network('10.0.0.0/8'),
|
ip_network("10.0.0.0/8"),
|
||||||
ip_network('172.16.0.0/12'),
|
ip_network("172.16.0.0/12"),
|
||||||
ip_network('192.168.0.0/16'),
|
ip_network("192.168.0.0/16"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -32,8 +29,8 @@ async def verify_redirect_uri(hass, client_id, redirect_uri):
|
||||||
|
|
||||||
# Verify redirect url and client url have same scheme and domain.
|
# Verify redirect url and client url have same scheme and domain.
|
||||||
is_valid = (
|
is_valid = (
|
||||||
client_id_parts.scheme == redirect_parts.scheme and
|
client_id_parts.scheme == redirect_parts.scheme
|
||||||
client_id_parts.netloc == redirect_parts.netloc
|
and client_id_parts.netloc == redirect_parts.netloc
|
||||||
)
|
)
|
||||||
|
|
||||||
if is_valid:
|
if is_valid:
|
||||||
|
|
@ -56,13 +53,13 @@ class LinkTagParser(HTMLParser):
|
||||||
|
|
||||||
def handle_starttag(self, tag, attrs):
|
def handle_starttag(self, tag, attrs):
|
||||||
"""Handle finding a start tag."""
|
"""Handle finding a start tag."""
|
||||||
if tag != 'link':
|
if tag != "link":
|
||||||
return
|
return
|
||||||
|
|
||||||
attrs = dict(attrs)
|
attrs = dict(attrs)
|
||||||
|
|
||||||
if attrs.get('rel') == self.rel:
|
if attrs.get("rel") == self.rel:
|
||||||
self.found.append(attrs.get('href'))
|
self.found.append(attrs.get("href"))
|
||||||
|
|
||||||
|
|
||||||
async def fetch_redirect_uris(hass, url):
|
async def fetch_redirect_uris(hass, url):
|
||||||
|
|
@ -77,7 +74,7 @@ async def fetch_redirect_uris(hass, url):
|
||||||
|
|
||||||
We do not implement extracting redirect uris from headers.
|
We do not implement extracting redirect uris from headers.
|
||||||
"""
|
"""
|
||||||
parser = LinkTagParser('redirect_uri')
|
parser = LinkTagParser("redirect_uri")
|
||||||
chunks = 0
|
chunks = 0
|
||||||
try:
|
try:
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
|
|
@ -119,8 +116,8 @@ def _parse_url(url):
|
||||||
|
|
||||||
# If a URL with no path component is ever encountered,
|
# If a URL with no path component is ever encountered,
|
||||||
# it MUST be treated as if it had the path /.
|
# it MUST be treated as if it had the path /.
|
||||||
if parts.path == '':
|
if parts.path == "":
|
||||||
parts = parts._replace(path='/')
|
parts = parts._replace(path="/")
|
||||||
|
|
||||||
return parts
|
return parts
|
||||||
|
|
||||||
|
|
@ -134,34 +131,35 @@ def _parse_client_id(client_id):
|
||||||
|
|
||||||
# Client identifier URLs
|
# Client identifier URLs
|
||||||
# MUST have either an https or http scheme
|
# MUST have either an https or http scheme
|
||||||
if parts.scheme not in ('http', 'https'):
|
if parts.scheme not in ("http", "https"):
|
||||||
raise ValueError()
|
raise ValueError()
|
||||||
|
|
||||||
# MUST contain a path component
|
# MUST contain a path component
|
||||||
# Handled by url canonicalization.
|
# Handled by url canonicalization.
|
||||||
|
|
||||||
# MUST NOT contain single-dot or double-dot path segments
|
# MUST NOT contain single-dot or double-dot path segments
|
||||||
if any(segment in ('.', '..') for segment in parts.path.split('/')):
|
if any(segment in (".", "..") for segment in parts.path.split("/")):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
'Client ID cannot contain single-dot or double-dot path segments')
|
"Client ID cannot contain single-dot or double-dot path segments"
|
||||||
|
)
|
||||||
|
|
||||||
# MUST NOT contain a fragment component
|
# MUST NOT contain a fragment component
|
||||||
if parts.fragment != '':
|
if parts.fragment != "":
|
||||||
raise ValueError('Client ID cannot contain a fragment')
|
raise ValueError("Client ID cannot contain a fragment")
|
||||||
|
|
||||||
# MUST NOT contain a username or password component
|
# MUST NOT contain a username or password component
|
||||||
if parts.username is not None:
|
if parts.username is not None:
|
||||||
raise ValueError('Client ID cannot contain username')
|
raise ValueError("Client ID cannot contain username")
|
||||||
|
|
||||||
if parts.password is not None:
|
if parts.password is not None:
|
||||||
raise ValueError('Client ID cannot contain password')
|
raise ValueError("Client ID cannot contain password")
|
||||||
|
|
||||||
# MAY contain a port
|
# MAY contain a port
|
||||||
try:
|
try:
|
||||||
# parts raises ValueError when port cannot be parsed as int
|
# parts raises ValueError when port cannot be parsed as int
|
||||||
parts.port
|
parts.port
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise ValueError('Client ID contains invalid port')
|
raise ValueError("Client ID contains invalid port")
|
||||||
|
|
||||||
# Additionally, hostnames
|
# Additionally, hostnames
|
||||||
# MUST be domain names or a loopback interface and
|
# MUST be domain names or a loopback interface and
|
||||||
|
|
@ -177,7 +175,7 @@ def _parse_client_id(client_id):
|
||||||
netloc = parts.netloc
|
netloc = parts.netloc
|
||||||
|
|
||||||
# Strip the [, ] from ipv6 addresses before parsing
|
# Strip the [, ] from ipv6 addresses before parsing
|
||||||
if netloc[0] == '[' and netloc[-1] == ']':
|
if netloc[0] == "[" and netloc[-1] == "]":
|
||||||
netloc = netloc[1:-1]
|
netloc = netloc[1:-1]
|
||||||
|
|
||||||
address = ip_address(netloc)
|
address = ip_address(netloc)
|
||||||
|
|
@ -185,9 +183,11 @@ def _parse_client_id(client_id):
|
||||||
# Not an ip address
|
# Not an ip address
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if (address is None or
|
if (
|
||||||
address in ALLOWED_IPS or
|
address is None
|
||||||
any(address in network for network in ALLOWED_NETWORKS)):
|
or address in ALLOWED_IPS
|
||||||
|
or any(address in network for network in ALLOWED_NETWORKS)
|
||||||
|
):
|
||||||
return parts
|
return parts
|
||||||
|
|
||||||
raise ValueError('Hostname should be a domain name or local IP address')
|
raise ValueError("Hostname should be a domain name or local IP address")
|
||||||
|
|
|
||||||
|
|
@ -71,8 +71,7 @@ import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant import data_entry_flow
|
from homeassistant import data_entry_flow
|
||||||
from homeassistant.components.http import KEY_REAL_IP
|
from homeassistant.components.http import KEY_REAL_IP
|
||||||
from homeassistant.components.http.ban import process_wrong_login, \
|
from homeassistant.components.http.ban import process_wrong_login, log_invalid_auth
|
||||||
log_invalid_auth
|
|
||||||
from homeassistant.components.http.data_validator import RequestDataValidator
|
from homeassistant.components.http.data_validator import RequestDataValidator
|
||||||
from homeassistant.components.http.view import HomeAssistantView
|
from homeassistant.components.http.view import HomeAssistantView
|
||||||
from . import indieauth
|
from . import indieauth
|
||||||
|
|
@ -82,55 +81,55 @@ async def async_setup(hass, store_result):
|
||||||
"""Component to allow users to login."""
|
"""Component to allow users to login."""
|
||||||
hass.http.register_view(AuthProvidersView)
|
hass.http.register_view(AuthProvidersView)
|
||||||
hass.http.register_view(LoginFlowIndexView(hass.auth.login_flow))
|
hass.http.register_view(LoginFlowIndexView(hass.auth.login_flow))
|
||||||
hass.http.register_view(
|
hass.http.register_view(LoginFlowResourceView(hass.auth.login_flow, store_result))
|
||||||
LoginFlowResourceView(hass.auth.login_flow, store_result))
|
|
||||||
|
|
||||||
|
|
||||||
class AuthProvidersView(HomeAssistantView):
|
class AuthProvidersView(HomeAssistantView):
|
||||||
"""View to get available auth providers."""
|
"""View to get available auth providers."""
|
||||||
|
|
||||||
url = '/auth/providers'
|
url = "/auth/providers"
|
||||||
name = 'api:auth:providers'
|
name = "api:auth:providers"
|
||||||
requires_auth = False
|
requires_auth = False
|
||||||
|
|
||||||
async def get(self, request):
|
async def get(self, request):
|
||||||
"""Get available auth providers."""
|
"""Get available auth providers."""
|
||||||
hass = request.app['hass']
|
hass = request.app["hass"]
|
||||||
|
|
||||||
if not hass.components.onboarding.async_is_onboarded():
|
if not hass.components.onboarding.async_is_onboarded():
|
||||||
return self.json_message(
|
return self.json_message(
|
||||||
message='Onboarding not finished',
|
message="Onboarding not finished",
|
||||||
status_code=400,
|
status_code=400,
|
||||||
message_code='onboarding_required'
|
message_code="onboarding_required",
|
||||||
)
|
)
|
||||||
|
|
||||||
return self.json([{
|
return self.json(
|
||||||
'name': provider.name,
|
[
|
||||||
'id': provider.id,
|
{"name": provider.name, "id": provider.id, "type": provider.type}
|
||||||
'type': provider.type,
|
for provider in hass.auth.auth_providers
|
||||||
} for provider in hass.auth.auth_providers])
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _prepare_result_json(result):
|
def _prepare_result_json(result):
|
||||||
"""Convert result to JSON."""
|
"""Convert result to JSON."""
|
||||||
if result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
|
if result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
|
||||||
data = result.copy()
|
data = result.copy()
|
||||||
data.pop('result')
|
data.pop("result")
|
||||||
data.pop('data')
|
data.pop("data")
|
||||||
return data
|
return data
|
||||||
|
|
||||||
if result['type'] != data_entry_flow.RESULT_TYPE_FORM:
|
if result["type"] != data_entry_flow.RESULT_TYPE_FORM:
|
||||||
return result
|
return result
|
||||||
|
|
||||||
import voluptuous_serialize
|
import voluptuous_serialize
|
||||||
|
|
||||||
data = result.copy()
|
data = result.copy()
|
||||||
|
|
||||||
schema = data['data_schema']
|
schema = data["data_schema"]
|
||||||
if schema is None:
|
if schema is None:
|
||||||
data['data_schema'] = []
|
data["data_schema"] = []
|
||||||
else:
|
else:
|
||||||
data['data_schema'] = voluptuous_serialize.convert(schema)
|
data["data_schema"] = voluptuous_serialize.convert(schema)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
@ -138,8 +137,8 @@ def _prepare_result_json(result):
|
||||||
class LoginFlowIndexView(HomeAssistantView):
|
class LoginFlowIndexView(HomeAssistantView):
|
||||||
"""View to create a config flow."""
|
"""View to create a config flow."""
|
||||||
|
|
||||||
url = '/auth/login_flow'
|
url = "/auth/login_flow"
|
||||||
name = 'api:auth:login_flow'
|
name = "api:auth:login_flow"
|
||||||
requires_auth = False
|
requires_auth = False
|
||||||
|
|
||||||
def __init__(self, flow_mgr):
|
def __init__(self, flow_mgr):
|
||||||
|
|
@ -150,34 +149,41 @@ class LoginFlowIndexView(HomeAssistantView):
|
||||||
"""Do not allow index of flows in progress."""
|
"""Do not allow index of flows in progress."""
|
||||||
return web.Response(status=405)
|
return web.Response(status=405)
|
||||||
|
|
||||||
@RequestDataValidator(vol.Schema({
|
@RequestDataValidator(
|
||||||
vol.Required('client_id'): str,
|
vol.Schema(
|
||||||
vol.Required('handler'): vol.Any(str, list),
|
{
|
||||||
vol.Required('redirect_uri'): str,
|
vol.Required("client_id"): str,
|
||||||
vol.Optional('type', default='authorize'): str,
|
vol.Required("handler"): vol.Any(str, list),
|
||||||
}))
|
vol.Required("redirect_uri"): str,
|
||||||
|
vol.Optional("type", default="authorize"): str,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
@log_invalid_auth
|
@log_invalid_auth
|
||||||
async def post(self, request, data):
|
async def post(self, request, data):
|
||||||
"""Create a new login flow."""
|
"""Create a new login flow."""
|
||||||
if not await indieauth.verify_redirect_uri(
|
if not await indieauth.verify_redirect_uri(
|
||||||
request.app['hass'], data['client_id'], data['redirect_uri']):
|
request.app["hass"], data["client_id"], data["redirect_uri"]
|
||||||
return self.json_message('invalid client id or redirect uri', 400)
|
):
|
||||||
|
return self.json_message("invalid client id or redirect uri", 400)
|
||||||
|
|
||||||
if isinstance(data['handler'], list):
|
if isinstance(data["handler"], list):
|
||||||
handler = tuple(data['handler'])
|
handler = tuple(data["handler"])
|
||||||
else:
|
else:
|
||||||
handler = data['handler']
|
handler = data["handler"]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = await self._flow_mgr.async_init(
|
result = await self._flow_mgr.async_init(
|
||||||
handler, context={
|
handler,
|
||||||
'ip_address': request[KEY_REAL_IP],
|
context={
|
||||||
'credential_only': data.get('type') == 'link_user',
|
"ip_address": request[KEY_REAL_IP],
|
||||||
})
|
"credential_only": data.get("type") == "link_user",
|
||||||
|
},
|
||||||
|
)
|
||||||
except data_entry_flow.UnknownHandler:
|
except data_entry_flow.UnknownHandler:
|
||||||
return self.json_message('Invalid handler specified', 404)
|
return self.json_message("Invalid handler specified", 404)
|
||||||
except data_entry_flow.UnknownStep:
|
except data_entry_flow.UnknownStep:
|
||||||
return self.json_message('Handler does not support init', 400)
|
return self.json_message("Handler does not support init", 400)
|
||||||
|
|
||||||
return self.json(_prepare_result_json(result))
|
return self.json(_prepare_result_json(result))
|
||||||
|
|
||||||
|
|
@ -185,8 +191,8 @@ class LoginFlowIndexView(HomeAssistantView):
|
||||||
class LoginFlowResourceView(HomeAssistantView):
|
class LoginFlowResourceView(HomeAssistantView):
|
||||||
"""View to interact with the flow manager."""
|
"""View to interact with the flow manager."""
|
||||||
|
|
||||||
url = '/auth/login_flow/{flow_id}'
|
url = "/auth/login_flow/{flow_id}"
|
||||||
name = 'api:auth:login_flow:resource'
|
name = "api:auth:login_flow:resource"
|
||||||
requires_auth = False
|
requires_auth = False
|
||||||
|
|
||||||
def __init__(self, flow_mgr, store_result):
|
def __init__(self, flow_mgr, store_result):
|
||||||
|
|
@ -196,43 +202,43 @@ class LoginFlowResourceView(HomeAssistantView):
|
||||||
|
|
||||||
async def get(self, request):
|
async def get(self, request):
|
||||||
"""Do not allow getting status of a flow in progress."""
|
"""Do not allow getting status of a flow in progress."""
|
||||||
return self.json_message('Invalid flow specified', 404)
|
return self.json_message("Invalid flow specified", 404)
|
||||||
|
|
||||||
@RequestDataValidator(vol.Schema({
|
@RequestDataValidator(vol.Schema({"client_id": str}, extra=vol.ALLOW_EXTRA))
|
||||||
'client_id': str
|
|
||||||
}, extra=vol.ALLOW_EXTRA))
|
|
||||||
@log_invalid_auth
|
@log_invalid_auth
|
||||||
async def post(self, request, flow_id, data):
|
async def post(self, request, flow_id, data):
|
||||||
"""Handle progressing a login flow request."""
|
"""Handle progressing a login flow request."""
|
||||||
client_id = data.pop('client_id')
|
client_id = data.pop("client_id")
|
||||||
|
|
||||||
if not indieauth.verify_client_id(client_id):
|
if not indieauth.verify_client_id(client_id):
|
||||||
return self.json_message('Invalid client id', 400)
|
return self.json_message("Invalid client id", 400)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# do not allow change ip during login flow
|
# do not allow change ip during login flow
|
||||||
for flow in self._flow_mgr.async_progress():
|
for flow in self._flow_mgr.async_progress():
|
||||||
if (flow['flow_id'] == flow_id and
|
if flow["flow_id"] == flow_id and flow["context"][
|
||||||
flow['context']['ip_address'] !=
|
"ip_address"
|
||||||
request.get(KEY_REAL_IP)):
|
] != request.get(KEY_REAL_IP):
|
||||||
return self.json_message('IP address changed', 400)
|
return self.json_message("IP address changed", 400)
|
||||||
|
|
||||||
result = await self._flow_mgr.async_configure(flow_id, data)
|
result = await self._flow_mgr.async_configure(flow_id, data)
|
||||||
except data_entry_flow.UnknownFlow:
|
except data_entry_flow.UnknownFlow:
|
||||||
return self.json_message('Invalid flow specified', 404)
|
return self.json_message("Invalid flow specified", 404)
|
||||||
except vol.Invalid:
|
except vol.Invalid:
|
||||||
return self.json_message('User input malformed', 400)
|
return self.json_message("User input malformed", 400)
|
||||||
|
|
||||||
if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
|
if result["type"] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
|
||||||
# @log_invalid_auth does not work here since it returns HTTP 200
|
# @log_invalid_auth does not work here since it returns HTTP 200
|
||||||
# need manually log failed login attempts
|
# need manually log failed login attempts
|
||||||
if result['errors'] is not None and \
|
if (
|
||||||
result['errors'].get('base') == 'invalid_auth':
|
result["errors"] is not None
|
||||||
|
and result["errors"].get("base") == "invalid_auth"
|
||||||
|
):
|
||||||
await process_wrong_login(request)
|
await process_wrong_login(request)
|
||||||
return self.json(_prepare_result_json(result))
|
return self.json(_prepare_result_json(result))
|
||||||
|
|
||||||
result.pop('data')
|
result.pop("data")
|
||||||
result['result'] = self._store_result(client_id, result['result'])
|
result["result"] = self._store_result(client_id, result["result"])
|
||||||
|
|
||||||
return self.json(result)
|
return self.json(result)
|
||||||
|
|
||||||
|
|
@ -241,6 +247,6 @@ class LoginFlowResourceView(HomeAssistantView):
|
||||||
try:
|
try:
|
||||||
self._flow_mgr.async_abort(flow_id)
|
self._flow_mgr.async_abort(flow_id)
|
||||||
except data_entry_flow.UnknownFlow:
|
except data_entry_flow.UnknownFlow:
|
||||||
return self.json_message('Invalid flow specified', 404)
|
return self.json_message("Invalid flow specified", 404)
|
||||||
|
|
||||||
return self.json_message('Flow aborted')
|
return self.json_message("Flow aborted")
|
||||||
|
|
|
||||||
|
|
@ -7,82 +7,93 @@ from homeassistant import data_entry_flow
|
||||||
from homeassistant.components import websocket_api
|
from homeassistant.components import websocket_api
|
||||||
from homeassistant.core import callback, HomeAssistant
|
from homeassistant.core import callback, HomeAssistant
|
||||||
|
|
||||||
WS_TYPE_SETUP_MFA = 'auth/setup_mfa'
|
WS_TYPE_SETUP_MFA = "auth/setup_mfa"
|
||||||
SCHEMA_WS_SETUP_MFA = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
SCHEMA_WS_SETUP_MFA = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
|
||||||
vol.Required('type'): WS_TYPE_SETUP_MFA,
|
{
|
||||||
vol.Exclusive('mfa_module_id', 'module_or_flow_id'): str,
|
vol.Required("type"): WS_TYPE_SETUP_MFA,
|
||||||
vol.Exclusive('flow_id', 'module_or_flow_id'): str,
|
vol.Exclusive("mfa_module_id", "module_or_flow_id"): str,
|
||||||
vol.Optional('user_input'): object,
|
vol.Exclusive("flow_id", "module_or_flow_id"): str,
|
||||||
})
|
vol.Optional("user_input"): object,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
WS_TYPE_DEPOSE_MFA = 'auth/depose_mfa'
|
WS_TYPE_DEPOSE_MFA = "auth/depose_mfa"
|
||||||
SCHEMA_WS_DEPOSE_MFA = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
SCHEMA_WS_DEPOSE_MFA = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
|
||||||
vol.Required('type'): WS_TYPE_DEPOSE_MFA,
|
{vol.Required("type"): WS_TYPE_DEPOSE_MFA, vol.Required("mfa_module_id"): str}
|
||||||
vol.Required('mfa_module_id'): str,
|
)
|
||||||
})
|
|
||||||
|
|
||||||
DATA_SETUP_FLOW_MGR = 'auth_mfa_setup_flow_manager'
|
DATA_SETUP_FLOW_MGR = "auth_mfa_setup_flow_manager"
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup(hass):
|
async def async_setup(hass):
|
||||||
"""Init mfa setup flow manager."""
|
"""Init mfa setup flow manager."""
|
||||||
|
|
||||||
async def _async_create_setup_flow(handler, context, data):
|
async def _async_create_setup_flow(handler, context, data):
|
||||||
"""Create a setup flow. hanlder is a mfa module."""
|
"""Create a setup flow. hanlder is a mfa module."""
|
||||||
mfa_module = hass.auth.get_auth_mfa_module(handler)
|
mfa_module = hass.auth.get_auth_mfa_module(handler)
|
||||||
if mfa_module is None:
|
if mfa_module is None:
|
||||||
raise ValueError('Mfa module {} is not found'.format(handler))
|
raise ValueError("Mfa module {} is not found".format(handler))
|
||||||
|
|
||||||
user_id = data.pop('user_id')
|
user_id = data.pop("user_id")
|
||||||
return await mfa_module.async_setup_flow(user_id)
|
return await mfa_module.async_setup_flow(user_id)
|
||||||
|
|
||||||
async def _async_finish_setup_flow(flow, flow_result):
|
async def _async_finish_setup_flow(flow, flow_result):
|
||||||
_LOGGER.debug('flow_result: %s', flow_result)
|
_LOGGER.debug("flow_result: %s", flow_result)
|
||||||
return flow_result
|
return flow_result
|
||||||
|
|
||||||
hass.data[DATA_SETUP_FLOW_MGR] = data_entry_flow.FlowManager(
|
hass.data[DATA_SETUP_FLOW_MGR] = data_entry_flow.FlowManager(
|
||||||
hass, _async_create_setup_flow, _async_finish_setup_flow)
|
hass, _async_create_setup_flow, _async_finish_setup_flow
|
||||||
|
)
|
||||||
|
|
||||||
hass.components.websocket_api.async_register_command(
|
hass.components.websocket_api.async_register_command(
|
||||||
WS_TYPE_SETUP_MFA, websocket_setup_mfa, SCHEMA_WS_SETUP_MFA)
|
WS_TYPE_SETUP_MFA, websocket_setup_mfa, SCHEMA_WS_SETUP_MFA
|
||||||
|
)
|
||||||
|
|
||||||
hass.components.websocket_api.async_register_command(
|
hass.components.websocket_api.async_register_command(
|
||||||
WS_TYPE_DEPOSE_MFA, websocket_depose_mfa, SCHEMA_WS_DEPOSE_MFA)
|
WS_TYPE_DEPOSE_MFA, websocket_depose_mfa, SCHEMA_WS_DEPOSE_MFA
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
@websocket_api.ws_require_user(allow_system_user=False)
|
@websocket_api.ws_require_user(allow_system_user=False)
|
||||||
def websocket_setup_mfa(
|
def websocket_setup_mfa(
|
||||||
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg):
|
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg
|
||||||
|
):
|
||||||
"""Return a setup flow for mfa auth module."""
|
"""Return a setup flow for mfa auth module."""
|
||||||
|
|
||||||
async def async_setup_flow(msg):
|
async def async_setup_flow(msg):
|
||||||
"""Return a setup flow for mfa auth module."""
|
"""Return a setup flow for mfa auth module."""
|
||||||
flow_manager = hass.data[DATA_SETUP_FLOW_MGR]
|
flow_manager = hass.data[DATA_SETUP_FLOW_MGR]
|
||||||
|
|
||||||
flow_id = msg.get('flow_id')
|
flow_id = msg.get("flow_id")
|
||||||
if flow_id is not None:
|
if flow_id is not None:
|
||||||
result = await flow_manager.async_configure(
|
result = await flow_manager.async_configure(flow_id, msg.get("user_input"))
|
||||||
flow_id, msg.get('user_input'))
|
|
||||||
connection.send_message_outside(
|
connection.send_message_outside(
|
||||||
websocket_api.result_message(
|
websocket_api.result_message(msg["id"], _prepare_result_json(result))
|
||||||
msg['id'], _prepare_result_json(result)))
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
mfa_module_id = msg.get('mfa_module_id')
|
mfa_module_id = msg.get("mfa_module_id")
|
||||||
mfa_module = hass.auth.get_auth_mfa_module(mfa_module_id)
|
mfa_module = hass.auth.get_auth_mfa_module(mfa_module_id)
|
||||||
if mfa_module is None:
|
if mfa_module is None:
|
||||||
connection.send_message_outside(websocket_api.error_message(
|
connection.send_message_outside(
|
||||||
msg['id'], 'no_module',
|
websocket_api.error_message(
|
||||||
'MFA module {} is not found'.format(mfa_module_id)))
|
msg["id"],
|
||||||
|
"no_module",
|
||||||
|
"MFA module {} is not found".format(mfa_module_id),
|
||||||
|
)
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
result = await flow_manager.async_init(
|
result = await flow_manager.async_init(
|
||||||
mfa_module_id, data={'user_id': connection.user.id})
|
mfa_module_id, data={"user_id": connection.user.id}
|
||||||
|
)
|
||||||
|
|
||||||
connection.send_message_outside(
|
connection.send_message_outside(
|
||||||
websocket_api.result_message(
|
websocket_api.result_message(msg["id"], _prepare_result_json(result))
|
||||||
msg['id'], _prepare_result_json(result)))
|
)
|
||||||
|
|
||||||
hass.async_create_task(async_setup_flow(msg))
|
hass.async_create_task(async_setup_flow(msg))
|
||||||
|
|
||||||
|
|
@ -90,45 +101,49 @@ def websocket_setup_mfa(
|
||||||
@callback
|
@callback
|
||||||
@websocket_api.ws_require_user(allow_system_user=False)
|
@websocket_api.ws_require_user(allow_system_user=False)
|
||||||
def websocket_depose_mfa(
|
def websocket_depose_mfa(
|
||||||
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg):
|
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg
|
||||||
|
):
|
||||||
"""Remove user from mfa module."""
|
"""Remove user from mfa module."""
|
||||||
|
|
||||||
async def async_depose(msg):
|
async def async_depose(msg):
|
||||||
"""Remove user from mfa auth module."""
|
"""Remove user from mfa auth module."""
|
||||||
mfa_module_id = msg['mfa_module_id']
|
mfa_module_id = msg["mfa_module_id"]
|
||||||
try:
|
try:
|
||||||
await hass.auth.async_disable_user_mfa(
|
await hass.auth.async_disable_user_mfa(
|
||||||
connection.user, msg['mfa_module_id'])
|
connection.user, msg["mfa_module_id"]
|
||||||
|
)
|
||||||
except ValueError as err:
|
except ValueError as err:
|
||||||
connection.send_message_outside(websocket_api.error_message(
|
connection.send_message_outside(
|
||||||
msg['id'], 'disable_failed',
|
websocket_api.error_message(
|
||||||
'Cannot disable MFA Module {}: {}'.format(
|
msg["id"],
|
||||||
mfa_module_id, err)))
|
"disable_failed",
|
||||||
|
"Cannot disable MFA Module {}: {}".format(mfa_module_id, err),
|
||||||
|
)
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
connection.send_message_outside(
|
connection.send_message_outside(websocket_api.result_message(msg["id"], "done"))
|
||||||
websocket_api.result_message(
|
|
||||||
msg['id'], 'done'))
|
|
||||||
|
|
||||||
hass.async_create_task(async_depose(msg))
|
hass.async_create_task(async_depose(msg))
|
||||||
|
|
||||||
|
|
||||||
def _prepare_result_json(result):
|
def _prepare_result_json(result):
|
||||||
"""Convert result to JSON."""
|
"""Convert result to JSON."""
|
||||||
if result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
|
if result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
|
||||||
data = result.copy()
|
data = result.copy()
|
||||||
return data
|
return data
|
||||||
|
|
||||||
if result['type'] != data_entry_flow.RESULT_TYPE_FORM:
|
if result["type"] != data_entry_flow.RESULT_TYPE_FORM:
|
||||||
return result
|
return result
|
||||||
|
|
||||||
import voluptuous_serialize
|
import voluptuous_serialize
|
||||||
|
|
||||||
data = result.copy()
|
data = result.copy()
|
||||||
|
|
||||||
schema = data['data_schema']
|
schema = data["data_schema"]
|
||||||
if schema is None:
|
if schema is None:
|
||||||
data['data_schema'] = []
|
data["data_schema"] = []
|
||||||
else:
|
else:
|
||||||
data['data_schema'] = voluptuous_serialize.convert(schema)
|
data["data_schema"] = voluptuous_serialize.convert(schema)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,16 @@ from homeassistant.setup import async_prepare_setup_platform
|
||||||
from homeassistant.core import CoreState
|
from homeassistant.core import CoreState
|
||||||
from homeassistant.loader import bind_hass
|
from homeassistant.loader import bind_hass
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ENTITY_ID, CONF_PLATFORM, STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF,
|
ATTR_ENTITY_ID,
|
||||||
SERVICE_TOGGLE, SERVICE_RELOAD, EVENT_HOMEASSISTANT_START, CONF_ID)
|
CONF_PLATFORM,
|
||||||
|
STATE_ON,
|
||||||
|
SERVICE_TURN_ON,
|
||||||
|
SERVICE_TURN_OFF,
|
||||||
|
SERVICE_TOGGLE,
|
||||||
|
SERVICE_RELOAD,
|
||||||
|
EVENT_HOMEASSISTANT_START,
|
||||||
|
CONF_ID,
|
||||||
|
)
|
||||||
from homeassistant.components import logbook
|
from homeassistant.components import logbook
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers import extract_domain_configs, script, condition
|
from homeassistant.helpers import extract_domain_configs, script, condition
|
||||||
|
|
@ -26,32 +34,32 @@ from homeassistant.helpers.restore_state import async_get_last_state
|
||||||
from homeassistant.util.dt import utcnow
|
from homeassistant.util.dt import utcnow
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
DOMAIN = 'automation'
|
DOMAIN = "automation"
|
||||||
DEPENDENCIES = ['group']
|
DEPENDENCIES = ["group"]
|
||||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
ENTITY_ID_FORMAT = DOMAIN + ".{}"
|
||||||
|
|
||||||
GROUP_NAME_ALL_AUTOMATIONS = 'all automations'
|
GROUP_NAME_ALL_AUTOMATIONS = "all automations"
|
||||||
|
|
||||||
CONF_ALIAS = 'alias'
|
CONF_ALIAS = "alias"
|
||||||
CONF_HIDE_ENTITY = 'hide_entity'
|
CONF_HIDE_ENTITY = "hide_entity"
|
||||||
|
|
||||||
CONF_CONDITION = 'condition'
|
CONF_CONDITION = "condition"
|
||||||
CONF_ACTION = 'action'
|
CONF_ACTION = "action"
|
||||||
CONF_TRIGGER = 'trigger'
|
CONF_TRIGGER = "trigger"
|
||||||
CONF_CONDITION_TYPE = 'condition_type'
|
CONF_CONDITION_TYPE = "condition_type"
|
||||||
CONF_INITIAL_STATE = 'initial_state'
|
CONF_INITIAL_STATE = "initial_state"
|
||||||
|
|
||||||
CONDITION_USE_TRIGGER_VALUES = 'use_trigger_values'
|
CONDITION_USE_TRIGGER_VALUES = "use_trigger_values"
|
||||||
CONDITION_TYPE_AND = 'and'
|
CONDITION_TYPE_AND = "and"
|
||||||
CONDITION_TYPE_OR = 'or'
|
CONDITION_TYPE_OR = "or"
|
||||||
|
|
||||||
DEFAULT_CONDITION_TYPE = CONDITION_TYPE_AND
|
DEFAULT_CONDITION_TYPE = CONDITION_TYPE_AND
|
||||||
DEFAULT_HIDE_ENTITY = False
|
DEFAULT_HIDE_ENTITY = False
|
||||||
DEFAULT_INITIAL_STATE = True
|
DEFAULT_INITIAL_STATE = True
|
||||||
|
|
||||||
ATTR_LAST_TRIGGERED = 'last_triggered'
|
ATTR_LAST_TRIGGERED = "last_triggered"
|
||||||
ATTR_VARIABLES = 'variables'
|
ATTR_VARIABLES = "variables"
|
||||||
SERVICE_TRIGGER = 'trigger'
|
SERVICE_TRIGGER = "trigger"
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -60,10 +68,10 @@ def _platform_validator(config):
|
||||||
"""Validate it is a valid platform."""
|
"""Validate it is a valid platform."""
|
||||||
try:
|
try:
|
||||||
platform = importlib.import_module(
|
platform = importlib.import_module(
|
||||||
'homeassistant.components.automation.{}'.format(
|
"homeassistant.components.automation.{}".format(config[CONF_PLATFORM])
|
||||||
config[CONF_PLATFORM]))
|
)
|
||||||
except ImportError:
|
except ImportError:
|
||||||
raise vol.Invalid('Invalid platform specified') from None
|
raise vol.Invalid("Invalid platform specified") from None
|
||||||
|
|
||||||
return platform.TRIGGER_SCHEMA(config)
|
return platform.TRIGGER_SCHEMA(config)
|
||||||
|
|
||||||
|
|
@ -72,35 +80,35 @@ _TRIGGER_SCHEMA = vol.All(
|
||||||
cv.ensure_list,
|
cv.ensure_list,
|
||||||
[
|
[
|
||||||
vol.All(
|
vol.All(
|
||||||
vol.Schema({
|
vol.Schema({vol.Required(CONF_PLATFORM): str}, extra=vol.ALLOW_EXTRA),
|
||||||
vol.Required(CONF_PLATFORM): str
|
_platform_validator,
|
||||||
}, extra=vol.ALLOW_EXTRA),
|
)
|
||||||
_platform_validator
|
],
|
||||||
),
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
_CONDITION_SCHEMA = vol.All(cv.ensure_list, [cv.CONDITION_SCHEMA])
|
_CONDITION_SCHEMA = vol.All(cv.ensure_list, [cv.CONDITION_SCHEMA])
|
||||||
|
|
||||||
PLATFORM_SCHEMA = vol.Schema({
|
PLATFORM_SCHEMA = vol.Schema(
|
||||||
# str on purpose
|
{
|
||||||
CONF_ID: str,
|
# str on purpose
|
||||||
CONF_ALIAS: cv.string,
|
CONF_ID: str,
|
||||||
vol.Optional(CONF_INITIAL_STATE): cv.boolean,
|
CONF_ALIAS: cv.string,
|
||||||
vol.Optional(CONF_HIDE_ENTITY, default=DEFAULT_HIDE_ENTITY): cv.boolean,
|
vol.Optional(CONF_INITIAL_STATE): cv.boolean,
|
||||||
vol.Required(CONF_TRIGGER): _TRIGGER_SCHEMA,
|
vol.Optional(CONF_HIDE_ENTITY, default=DEFAULT_HIDE_ENTITY): cv.boolean,
|
||||||
vol.Optional(CONF_CONDITION): _CONDITION_SCHEMA,
|
vol.Required(CONF_TRIGGER): _TRIGGER_SCHEMA,
|
||||||
vol.Required(CONF_ACTION): cv.SCRIPT_SCHEMA,
|
vol.Optional(CONF_CONDITION): _CONDITION_SCHEMA,
|
||||||
})
|
vol.Required(CONF_ACTION): cv.SCRIPT_SCHEMA,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
SERVICE_SCHEMA = vol.Schema({
|
SERVICE_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.entity_ids})
|
||||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
|
||||||
})
|
|
||||||
|
|
||||||
TRIGGER_SERVICE_SCHEMA = vol.Schema({
|
TRIGGER_SERVICE_SCHEMA = vol.Schema(
|
||||||
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
|
{
|
||||||
vol.Optional(ATTR_VARIABLES, default={}): dict,
|
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
|
||||||
})
|
vol.Optional(ATTR_VARIABLES, default={}): dict,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
RELOAD_SERVICE_SCHEMA = vol.Schema({})
|
RELOAD_SERVICE_SCHEMA = vol.Schema({})
|
||||||
|
|
||||||
|
|
@ -160,8 +168,9 @@ def async_reload(hass):
|
||||||
|
|
||||||
async def async_setup(hass, config):
|
async def async_setup(hass, config):
|
||||||
"""Set up the automation."""
|
"""Set up the automation."""
|
||||||
component = EntityComponent(_LOGGER, DOMAIN, hass,
|
component = EntityComponent(
|
||||||
group_name=GROUP_NAME_ALL_AUTOMATIONS)
|
_LOGGER, DOMAIN, hass, group_name=GROUP_NAME_ALL_AUTOMATIONS
|
||||||
|
)
|
||||||
|
|
||||||
await _async_process_config(hass, config, component)
|
await _async_process_config(hass, config, component)
|
||||||
|
|
||||||
|
|
@ -169,10 +178,13 @@ async def async_setup(hass, config):
|
||||||
"""Handle automation triggers."""
|
"""Handle automation triggers."""
|
||||||
tasks = []
|
tasks = []
|
||||||
for entity in component.async_extract_from_service(service_call):
|
for entity in component.async_extract_from_service(service_call):
|
||||||
tasks.append(entity.async_trigger(
|
tasks.append(
|
||||||
service_call.data.get(ATTR_VARIABLES),
|
entity.async_trigger(
|
||||||
skip_condition=True,
|
service_call.data.get(ATTR_VARIABLES),
|
||||||
context=service_call.context))
|
skip_condition=True,
|
||||||
|
context=service_call.context,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if tasks:
|
if tasks:
|
||||||
await asyncio.wait(tasks, loop=hass.loop)
|
await asyncio.wait(tasks, loop=hass.loop)
|
||||||
|
|
@ -180,7 +192,7 @@ async def async_setup(hass, config):
|
||||||
async def turn_onoff_service_handler(service_call):
|
async def turn_onoff_service_handler(service_call):
|
||||||
"""Handle automation turn on/off service calls."""
|
"""Handle automation turn on/off service calls."""
|
||||||
tasks = []
|
tasks = []
|
||||||
method = 'async_{}'.format(service_call.service)
|
method = "async_{}".format(service_call.service)
|
||||||
for entity in component.async_extract_from_service(service_call):
|
for entity in component.async_extract_from_service(service_call):
|
||||||
tasks.append(getattr(entity, method)())
|
tasks.append(getattr(entity, method)())
|
||||||
|
|
||||||
|
|
@ -207,21 +219,21 @@ async def async_setup(hass, config):
|
||||||
await _async_process_config(hass, conf, component)
|
await _async_process_config(hass, conf, component)
|
||||||
|
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
DOMAIN, SERVICE_TRIGGER, trigger_service_handler,
|
DOMAIN, SERVICE_TRIGGER, trigger_service_handler, schema=TRIGGER_SERVICE_SCHEMA
|
||||||
schema=TRIGGER_SERVICE_SCHEMA)
|
)
|
||||||
|
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
DOMAIN, SERVICE_RELOAD, reload_service_handler,
|
DOMAIN, SERVICE_RELOAD, reload_service_handler, schema=RELOAD_SERVICE_SCHEMA
|
||||||
schema=RELOAD_SERVICE_SCHEMA)
|
)
|
||||||
|
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
DOMAIN, SERVICE_TOGGLE, toggle_service_handler,
|
DOMAIN, SERVICE_TOGGLE, toggle_service_handler, schema=SERVICE_SCHEMA
|
||||||
schema=SERVICE_SCHEMA)
|
)
|
||||||
|
|
||||||
for service in (SERVICE_TURN_ON, SERVICE_TURN_OFF):
|
for service in (SERVICE_TURN_ON, SERVICE_TURN_OFF):
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
DOMAIN, service, turn_onoff_service_handler,
|
DOMAIN, service, turn_onoff_service_handler, schema=SERVICE_SCHEMA
|
||||||
schema=SERVICE_SCHEMA)
|
)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
@ -229,8 +241,16 @@ async def async_setup(hass, config):
|
||||||
class AutomationEntity(ToggleEntity):
|
class AutomationEntity(ToggleEntity):
|
||||||
"""Entity to show status of entity."""
|
"""Entity to show status of entity."""
|
||||||
|
|
||||||
def __init__(self, automation_id, name, async_attach_triggers, cond_func,
|
def __init__(
|
||||||
async_action, hidden, initial_state):
|
self,
|
||||||
|
automation_id,
|
||||||
|
name,
|
||||||
|
async_attach_triggers,
|
||||||
|
cond_func,
|
||||||
|
async_action,
|
||||||
|
hidden,
|
||||||
|
initial_state,
|
||||||
|
):
|
||||||
"""Initialize an automation entity."""
|
"""Initialize an automation entity."""
|
||||||
self._id = automation_id
|
self._id = automation_id
|
||||||
self._name = name
|
self._name = name
|
||||||
|
|
@ -255,9 +275,7 @@ class AutomationEntity(ToggleEntity):
|
||||||
@property
|
@property
|
||||||
def state_attributes(self):
|
def state_attributes(self):
|
||||||
"""Return the entity state attributes."""
|
"""Return the entity state attributes."""
|
||||||
return {
|
return {ATTR_LAST_TRIGGERED: self._last_triggered}
|
||||||
ATTR_LAST_TRIGGERED: self._last_triggered
|
|
||||||
}
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hidden(self) -> bool:
|
def hidden(self) -> bool:
|
||||||
|
|
@ -273,33 +291,43 @@ class AutomationEntity(ToggleEntity):
|
||||||
"""Startup with initial state or previous state."""
|
"""Startup with initial state or previous state."""
|
||||||
if self._initial_state is not None:
|
if self._initial_state is not None:
|
||||||
enable_automation = self._initial_state
|
enable_automation = self._initial_state
|
||||||
_LOGGER.debug("Automation %s initial state %s from config "
|
_LOGGER.debug(
|
||||||
"initial_state", self.entity_id, enable_automation)
|
"Automation %s initial state %s from config " "initial_state",
|
||||||
|
self.entity_id,
|
||||||
|
enable_automation,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
state = await async_get_last_state(self.hass, self.entity_id)
|
state = await async_get_last_state(self.hass, self.entity_id)
|
||||||
if state:
|
if state:
|
||||||
enable_automation = state.state == STATE_ON
|
enable_automation = state.state == STATE_ON
|
||||||
self._last_triggered = state.attributes.get('last_triggered')
|
self._last_triggered = state.attributes.get("last_triggered")
|
||||||
_LOGGER.debug("Automation %s initial state %s from recorder "
|
_LOGGER.debug(
|
||||||
"last state %s", self.entity_id,
|
"Automation %s initial state %s from recorder " "last state %s",
|
||||||
enable_automation, state)
|
self.entity_id,
|
||||||
|
enable_automation,
|
||||||
|
state,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
enable_automation = DEFAULT_INITIAL_STATE
|
enable_automation = DEFAULT_INITIAL_STATE
|
||||||
_LOGGER.debug("Automation %s initial state %s from default "
|
_LOGGER.debug(
|
||||||
"initial state", self.entity_id,
|
"Automation %s initial state %s from default " "initial state",
|
||||||
enable_automation)
|
self.entity_id,
|
||||||
|
enable_automation,
|
||||||
|
)
|
||||||
|
|
||||||
if not enable_automation:
|
if not enable_automation:
|
||||||
return
|
return
|
||||||
|
|
||||||
# HomeAssistant is starting up
|
# HomeAssistant is starting up
|
||||||
if self.hass.state == CoreState.not_running:
|
if self.hass.state == CoreState.not_running:
|
||||||
|
|
||||||
async def async_enable_automation(event):
|
async def async_enable_automation(event):
|
||||||
"""Start automation on startup."""
|
"""Start automation on startup."""
|
||||||
await self.async_enable()
|
await self.async_enable()
|
||||||
|
|
||||||
self.hass.bus.async_listen_once(
|
self.hass.bus.async_listen_once(
|
||||||
EVENT_HOMEASSISTANT_START, async_enable_automation)
|
EVENT_HOMEASSISTANT_START, async_enable_automation
|
||||||
|
)
|
||||||
|
|
||||||
# HomeAssistant is running
|
# HomeAssistant is running
|
||||||
else:
|
else:
|
||||||
|
|
@ -321,8 +349,7 @@ class AutomationEntity(ToggleEntity):
|
||||||
self._async_detach_triggers = None
|
self._async_detach_triggers = None
|
||||||
await self.async_update_ha_state()
|
await self.async_update_ha_state()
|
||||||
|
|
||||||
async def async_trigger(self, variables, skip_condition=False,
|
async def async_trigger(self, variables, skip_condition=False, context=None):
|
||||||
context=None):
|
|
||||||
"""Trigger automation.
|
"""Trigger automation.
|
||||||
|
|
||||||
This method is a coroutine.
|
This method is a coroutine.
|
||||||
|
|
@ -346,7 +373,8 @@ class AutomationEntity(ToggleEntity):
|
||||||
return
|
return
|
||||||
|
|
||||||
self._async_detach_triggers = await self._async_attach_triggers(
|
self._async_detach_triggers = await self._async_attach_triggers(
|
||||||
self.async_trigger)
|
self.async_trigger
|
||||||
|
)
|
||||||
await self.async_update_ha_state()
|
await self.async_update_ha_state()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
@ -355,9 +383,7 @@ class AutomationEntity(ToggleEntity):
|
||||||
if self._id is None:
|
if self._id is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return {
|
return {CONF_ID: self._id}
|
||||||
CONF_ID: self._id
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async def _async_process_config(hass, config, component):
|
async def _async_process_config(hass, config, component):
|
||||||
|
|
@ -372,14 +398,12 @@ async def _async_process_config(hass, config, component):
|
||||||
|
|
||||||
for list_no, config_block in enumerate(conf):
|
for list_no, config_block in enumerate(conf):
|
||||||
automation_id = config_block.get(CONF_ID)
|
automation_id = config_block.get(CONF_ID)
|
||||||
name = config_block.get(CONF_ALIAS) or "{} {}".format(config_key,
|
name = config_block.get(CONF_ALIAS) or "{} {}".format(config_key, list_no)
|
||||||
list_no)
|
|
||||||
|
|
||||||
hidden = config_block[CONF_HIDE_ENTITY]
|
hidden = config_block[CONF_HIDE_ENTITY]
|
||||||
initial_state = config_block.get(CONF_INITIAL_STATE)
|
initial_state = config_block.get(CONF_INITIAL_STATE)
|
||||||
|
|
||||||
action = _async_get_action(hass, config_block.get(CONF_ACTION, {}),
|
action = _async_get_action(hass, config_block.get(CONF_ACTION, {}), name)
|
||||||
name)
|
|
||||||
|
|
||||||
if CONF_CONDITION in config_block:
|
if CONF_CONDITION in config_block:
|
||||||
cond_func = _async_process_if(hass, config, config_block)
|
cond_func = _async_process_if(hass, config, config_block)
|
||||||
|
|
@ -387,17 +411,27 @@ async def _async_process_config(hass, config, component):
|
||||||
if cond_func is None:
|
if cond_func is None:
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
|
|
||||||
def cond_func(variables):
|
def cond_func(variables):
|
||||||
"""Condition will always pass."""
|
"""Condition will always pass."""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async_attach_triggers = partial(
|
async_attach_triggers = partial(
|
||||||
_async_process_trigger, hass, config,
|
_async_process_trigger,
|
||||||
config_block.get(CONF_TRIGGER, []), name
|
hass,
|
||||||
|
config,
|
||||||
|
config_block.get(CONF_TRIGGER, []),
|
||||||
|
name,
|
||||||
)
|
)
|
||||||
entity = AutomationEntity(
|
entity = AutomationEntity(
|
||||||
automation_id, name, async_attach_triggers, cond_func, action,
|
automation_id,
|
||||||
hidden, initial_state)
|
name,
|
||||||
|
async_attach_triggers,
|
||||||
|
cond_func,
|
||||||
|
action,
|
||||||
|
hidden,
|
||||||
|
initial_state,
|
||||||
|
)
|
||||||
|
|
||||||
entities.append(entity)
|
entities.append(entity)
|
||||||
|
|
||||||
|
|
@ -411,9 +445,8 @@ def _async_get_action(hass, config, name):
|
||||||
|
|
||||||
async def action(entity_id, variables, context):
|
async def action(entity_id, variables, context):
|
||||||
"""Execute an action."""
|
"""Execute an action."""
|
||||||
_LOGGER.info('Executing %s', name)
|
_LOGGER.info("Executing %s", name)
|
||||||
logbook.async_log_entry(
|
logbook.async_log_entry(hass, name, "has been triggered", DOMAIN, entity_id)
|
||||||
hass, name, 'has been triggered', DOMAIN, entity_id)
|
|
||||||
await script_obj.async_run(variables, context)
|
await script_obj.async_run(variables, context)
|
||||||
|
|
||||||
return action
|
return action
|
||||||
|
|
@ -428,7 +461,7 @@ def _async_process_if(hass, config, p_config):
|
||||||
try:
|
try:
|
||||||
checks.append(condition.async_from_config(if_config, False))
|
checks.append(condition.async_from_config(if_config, False))
|
||||||
except HomeAssistantError as ex:
|
except HomeAssistantError as ex:
|
||||||
_LOGGER.warning('Invalid condition: %s', ex)
|
_LOGGER.warning("Invalid condition: %s", ex)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def if_action(variables=None):
|
def if_action(variables=None):
|
||||||
|
|
@ -447,7 +480,8 @@ async def _async_process_trigger(hass, config, trigger_configs, name, action):
|
||||||
|
|
||||||
for conf in trigger_configs:
|
for conf in trigger_configs:
|
||||||
platform = await async_prepare_setup_platform(
|
platform = await async_prepare_setup_platform(
|
||||||
hass, config, DOMAIN, conf.get(CONF_PLATFORM))
|
hass, config, DOMAIN, conf.get(CONF_PLATFORM)
|
||||||
|
)
|
||||||
|
|
||||||
if platform is None:
|
if platform is None:
|
||||||
return None
|
return None
|
||||||
|
|
|
||||||
|
|
@ -13,25 +13,29 @@ from homeassistant.core import callback
|
||||||
from homeassistant.const import CONF_PLATFORM
|
from homeassistant.const import CONF_PLATFORM
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
|
|
||||||
CONF_EVENT_TYPE = 'event_type'
|
CONF_EVENT_TYPE = "event_type"
|
||||||
CONF_EVENT_DATA = 'event_data'
|
CONF_EVENT_DATA = "event_data"
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
TRIGGER_SCHEMA = vol.Schema({
|
TRIGGER_SCHEMA = vol.Schema(
|
||||||
vol.Required(CONF_PLATFORM): 'event',
|
{
|
||||||
vol.Required(CONF_EVENT_TYPE): cv.string,
|
vol.Required(CONF_PLATFORM): "event",
|
||||||
vol.Optional(CONF_EVENT_DATA): dict,
|
vol.Required(CONF_EVENT_TYPE): cv.string,
|
||||||
})
|
vol.Optional(CONF_EVENT_DATA): dict,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_trigger(hass, config, action):
|
def async_trigger(hass, config, action):
|
||||||
"""Listen for events based on configuration."""
|
"""Listen for events based on configuration."""
|
||||||
event_type = config.get(CONF_EVENT_TYPE)
|
event_type = config.get(CONF_EVENT_TYPE)
|
||||||
event_data_schema = vol.Schema(
|
event_data_schema = (
|
||||||
config.get(CONF_EVENT_DATA),
|
vol.Schema(config.get(CONF_EVENT_DATA), extra=vol.ALLOW_EXTRA)
|
||||||
extra=vol.ALLOW_EXTRA) if config.get(CONF_EVENT_DATA) else None
|
if config.get(CONF_EVENT_DATA)
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def handle_event(event):
|
def handle_event(event):
|
||||||
|
|
@ -45,11 +49,11 @@ def async_trigger(hass, config, action):
|
||||||
# If event data doesn't match requested schema, skip event
|
# If event data doesn't match requested schema, skip event
|
||||||
return
|
return
|
||||||
|
|
||||||
hass.async_run_job(action({
|
hass.async_run_job(
|
||||||
'trigger': {
|
action(
|
||||||
'platform': 'event',
|
{"trigger": {"platform": "event", "event": event}},
|
||||||
'event': event,
|
context=event.context,
|
||||||
},
|
)
|
||||||
}, context=event.context))
|
)
|
||||||
|
|
||||||
return hass.bus.async_listen(event_type, handle_event)
|
return hass.bus.async_listen(event_type, handle_event)
|
||||||
|
|
|
||||||
|
|
@ -10,17 +10,18 @@ import logging
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.core import callback, CoreState
|
from homeassistant.core import callback, CoreState
|
||||||
from homeassistant.const import (
|
from homeassistant.const import CONF_PLATFORM, CONF_EVENT, EVENT_HOMEASSISTANT_STOP
|
||||||
CONF_PLATFORM, CONF_EVENT, EVENT_HOMEASSISTANT_STOP)
|
|
||||||
|
|
||||||
EVENT_START = 'start'
|
EVENT_START = "start"
|
||||||
EVENT_SHUTDOWN = 'shutdown'
|
EVENT_SHUTDOWN = "shutdown"
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
TRIGGER_SCHEMA = vol.Schema({
|
TRIGGER_SCHEMA = vol.Schema(
|
||||||
vol.Required(CONF_PLATFORM): 'homeassistant',
|
{
|
||||||
vol.Required(CONF_EVENT): vol.Any(EVENT_START, EVENT_SHUTDOWN),
|
vol.Required(CONF_PLATFORM): "homeassistant",
|
||||||
})
|
vol.Required(CONF_EVENT): vol.Any(EVENT_START, EVENT_SHUTDOWN),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
|
|
@ -29,27 +30,24 @@ def async_trigger(hass, config, action):
|
||||||
event = config.get(CONF_EVENT)
|
event = config.get(CONF_EVENT)
|
||||||
|
|
||||||
if event == EVENT_SHUTDOWN:
|
if event == EVENT_SHUTDOWN:
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def hass_shutdown(event):
|
def hass_shutdown(event):
|
||||||
"""Execute when Home Assistant is shutting down."""
|
"""Execute when Home Assistant is shutting down."""
|
||||||
hass.async_run_job(action({
|
hass.async_run_job(
|
||||||
'trigger': {
|
action(
|
||||||
'platform': 'homeassistant',
|
{"trigger": {"platform": "homeassistant", "event": event}},
|
||||||
'event': event,
|
context=event.context,
|
||||||
},
|
)
|
||||||
}, context=event.context))
|
)
|
||||||
|
|
||||||
return hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP,
|
return hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, hass_shutdown)
|
||||||
hass_shutdown)
|
|
||||||
|
|
||||||
# Automation are enabled while hass is starting up, fire right away
|
# Automation are enabled while hass is starting up, fire right away
|
||||||
# Check state because a config reload shouldn't trigger it.
|
# Check state because a config reload shouldn't trigger it.
|
||||||
if hass.state == CoreState.starting:
|
if hass.state == CoreState.starting:
|
||||||
hass.async_run_job(action({
|
hass.async_run_job(
|
||||||
'trigger': {
|
action({"trigger": {"platform": "homeassistant", "event": event}})
|
||||||
'platform': 'homeassistant',
|
)
|
||||||
'event': event,
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
|
|
||||||
return lambda: None
|
return lambda: None
|
||||||
|
|
|
||||||
|
|
@ -15,22 +15,26 @@ import homeassistant.helpers.config_validation as cv
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
from homeassistant.helpers.event import track_point_in_utc_time
|
from homeassistant.helpers.event import track_point_in_utc_time
|
||||||
|
|
||||||
DEPENDENCIES = ['litejet']
|
DEPENDENCIES = ["litejet"]
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
CONF_NUMBER = 'number'
|
CONF_NUMBER = "number"
|
||||||
CONF_HELD_MORE_THAN = 'held_more_than'
|
CONF_HELD_MORE_THAN = "held_more_than"
|
||||||
CONF_HELD_LESS_THAN = 'held_less_than'
|
CONF_HELD_LESS_THAN = "held_less_than"
|
||||||
|
|
||||||
TRIGGER_SCHEMA = vol.Schema({
|
TRIGGER_SCHEMA = vol.Schema(
|
||||||
vol.Required(CONF_PLATFORM): 'litejet',
|
{
|
||||||
vol.Required(CONF_NUMBER): cv.positive_int,
|
vol.Required(CONF_PLATFORM): "litejet",
|
||||||
vol.Optional(CONF_HELD_MORE_THAN):
|
vol.Required(CONF_NUMBER): cv.positive_int,
|
||||||
vol.All(cv.time_period, cv.positive_timedelta),
|
vol.Optional(CONF_HELD_MORE_THAN): vol.All(
|
||||||
vol.Optional(CONF_HELD_LESS_THAN):
|
cv.time_period, cv.positive_timedelta
|
||||||
vol.All(cv.time_period, cv.positive_timedelta)
|
),
|
||||||
})
|
vol.Optional(CONF_HELD_LESS_THAN): vol.All(
|
||||||
|
cv.time_period, cv.positive_timedelta
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
|
|
@ -45,14 +49,17 @@ def async_trigger(hass, config, action):
|
||||||
@callback
|
@callback
|
||||||
def call_action():
|
def call_action():
|
||||||
"""Call action with right context."""
|
"""Call action with right context."""
|
||||||
hass.async_run_job(action, {
|
hass.async_run_job(
|
||||||
'trigger': {
|
action,
|
||||||
CONF_PLATFORM: 'litejet',
|
{
|
||||||
CONF_NUMBER: number,
|
"trigger": {
|
||||||
CONF_HELD_MORE_THAN: held_more_than,
|
CONF_PLATFORM: "litejet",
|
||||||
CONF_HELD_LESS_THAN: held_less_than
|
CONF_NUMBER: number,
|
||||||
|
CONF_HELD_MORE_THAN: held_more_than,
|
||||||
|
CONF_HELD_LESS_THAN: held_less_than,
|
||||||
|
}
|
||||||
},
|
},
|
||||||
})
|
)
|
||||||
|
|
||||||
# held_more_than and held_less_than: trigger on released (if in time range)
|
# held_more_than and held_less_than: trigger on released (if in time range)
|
||||||
# held_more_than: trigger after pressed with calculation
|
# held_more_than: trigger after pressed with calculation
|
||||||
|
|
@ -73,9 +80,8 @@ def async_trigger(hass, config, action):
|
||||||
hass.add_job(call_action)
|
hass.add_job(call_action)
|
||||||
if held_more_than is not None and held_less_than is None:
|
if held_more_than is not None and held_less_than is None:
|
||||||
cancel_pressed_more_than = track_point_in_utc_time(
|
cancel_pressed_more_than = track_point_in_utc_time(
|
||||||
hass,
|
hass, pressed_more_than_satisfied, dt_util.utcnow() + held_more_than
|
||||||
pressed_more_than_satisfied,
|
)
|
||||||
dt_util.utcnow() + held_more_than)
|
|
||||||
|
|
||||||
def released():
|
def released():
|
||||||
"""Handle the release of the LiteJet switch's button."""
|
"""Handle the release of the LiteJet switch's button."""
|
||||||
|
|
@ -90,8 +96,8 @@ def async_trigger(hass, config, action):
|
||||||
if held_more_than is None or held_time > held_more_than:
|
if held_more_than is None or held_time > held_more_than:
|
||||||
hass.add_job(call_action)
|
hass.add_job(call_action)
|
||||||
|
|
||||||
hass.data['litejet_system'].on_switch_pressed(number, pressed)
|
hass.data["litejet_system"].on_switch_pressed(number, pressed)
|
||||||
hass.data['litejet_system'].on_switch_released(number, released)
|
hass.data["litejet_system"].on_switch_released(number, released)
|
||||||
|
|
||||||
def async_remove():
|
def async_remove():
|
||||||
"""Remove all subscriptions used for this trigger."""
|
"""Remove all subscriptions used for this trigger."""
|
||||||
|
|
|
||||||
|
|
@ -11,18 +11,20 @@ import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.components import mqtt
|
from homeassistant.components import mqtt
|
||||||
from homeassistant.const import (CONF_PLATFORM, CONF_PAYLOAD)
|
from homeassistant.const import CONF_PLATFORM, CONF_PAYLOAD
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
DEPENDENCIES = ['mqtt']
|
DEPENDENCIES = ["mqtt"]
|
||||||
|
|
||||||
CONF_TOPIC = 'topic'
|
CONF_TOPIC = "topic"
|
||||||
|
|
||||||
TRIGGER_SCHEMA = vol.Schema({
|
TRIGGER_SCHEMA = vol.Schema(
|
||||||
vol.Required(CONF_PLATFORM): mqtt.DOMAIN,
|
{
|
||||||
vol.Required(CONF_TOPIC): mqtt.valid_subscribe_topic,
|
vol.Required(CONF_PLATFORM): mqtt.DOMAIN,
|
||||||
vol.Optional(CONF_PAYLOAD): cv.string,
|
vol.Required(CONF_TOPIC): mqtt.valid_subscribe_topic,
|
||||||
})
|
vol.Optional(CONF_PAYLOAD): cv.string,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
|
|
@ -36,21 +38,18 @@ def async_trigger(hass, config, action):
|
||||||
"""Listen for MQTT messages."""
|
"""Listen for MQTT messages."""
|
||||||
if payload is None or payload == msg_payload:
|
if payload is None or payload == msg_payload:
|
||||||
data = {
|
data = {
|
||||||
'platform': 'mqtt',
|
"platform": "mqtt",
|
||||||
'topic': msg_topic,
|
"topic": msg_topic,
|
||||||
'payload': msg_payload,
|
"payload": msg_payload,
|
||||||
'qos': qos,
|
"qos": qos,
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data['payload_json'] = json.loads(msg_payload)
|
data["payload_json"] = json.loads(msg_payload)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
hass.async_run_job(action, {
|
hass.async_run_job(action, {"trigger": data})
|
||||||
'trigger': data
|
|
||||||
})
|
|
||||||
|
|
||||||
remove = yield from mqtt.async_subscribe(
|
remove = yield from mqtt.async_subscribe(hass, topic, mqtt_automation_listener)
|
||||||
hass, topic, mqtt_automation_listener)
|
|
||||||
return remove
|
return remove
|
||||||
|
|
|
||||||
|
|
@ -11,20 +11,29 @@ import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_VALUE_TEMPLATE, CONF_PLATFORM, CONF_ENTITY_ID,
|
CONF_VALUE_TEMPLATE,
|
||||||
CONF_BELOW, CONF_ABOVE, CONF_FOR)
|
CONF_PLATFORM,
|
||||||
from homeassistant.helpers.event import (
|
CONF_ENTITY_ID,
|
||||||
async_track_state_change, async_track_same_state)
|
CONF_BELOW,
|
||||||
|
CONF_ABOVE,
|
||||||
|
CONF_FOR,
|
||||||
|
)
|
||||||
|
from homeassistant.helpers.event import async_track_state_change, async_track_same_state
|
||||||
from homeassistant.helpers import condition, config_validation as cv
|
from homeassistant.helpers import condition, config_validation as cv
|
||||||
|
|
||||||
TRIGGER_SCHEMA = vol.All(vol.Schema({
|
TRIGGER_SCHEMA = vol.All(
|
||||||
vol.Required(CONF_PLATFORM): 'numeric_state',
|
vol.Schema(
|
||||||
vol.Required(CONF_ENTITY_ID): cv.entity_ids,
|
{
|
||||||
vol.Optional(CONF_BELOW): vol.Coerce(float),
|
vol.Required(CONF_PLATFORM): "numeric_state",
|
||||||
vol.Optional(CONF_ABOVE): vol.Coerce(float),
|
vol.Required(CONF_ENTITY_ID): cv.entity_ids,
|
||||||
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
|
vol.Optional(CONF_BELOW): vol.Coerce(float),
|
||||||
vol.Optional(CONF_FOR): vol.All(cv.time_period, cv.positive_timedelta),
|
vol.Optional(CONF_ABOVE): vol.Coerce(float),
|
||||||
}), cv.has_at_least_one_key(CONF_BELOW, CONF_ABOVE))
|
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
|
||||||
|
vol.Optional(CONF_FOR): vol.All(cv.time_period, cv.positive_timedelta),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
cv.has_at_least_one_key(CONF_BELOW, CONF_ABOVE),
|
||||||
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -50,32 +59,39 @@ def async_trigger(hass, config, action):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
variables = {
|
variables = {
|
||||||
'trigger': {
|
"trigger": {
|
||||||
'platform': 'numeric_state',
|
"platform": "numeric_state",
|
||||||
'entity_id': entity,
|
"entity_id": entity,
|
||||||
'below': below,
|
"below": below,
|
||||||
'above': above,
|
"above": above,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return condition.async_numeric_state(
|
return condition.async_numeric_state(
|
||||||
hass, to_s, below, above, value_template, variables)
|
hass, to_s, below, above, value_template, variables
|
||||||
|
)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def state_automation_listener(entity, from_s, to_s):
|
def state_automation_listener(entity, from_s, to_s):
|
||||||
"""Listen for state changes and calls action."""
|
"""Listen for state changes and calls action."""
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def call_action():
|
def call_action():
|
||||||
"""Call action with right context."""
|
"""Call action with right context."""
|
||||||
hass.async_run_job(action({
|
hass.async_run_job(
|
||||||
'trigger': {
|
action(
|
||||||
'platform': 'numeric_state',
|
{
|
||||||
'entity_id': entity,
|
"trigger": {
|
||||||
'below': below,
|
"platform": "numeric_state",
|
||||||
'above': above,
|
"entity_id": entity,
|
||||||
'from_state': from_s,
|
"below": below,
|
||||||
'to_state': to_s,
|
"above": above,
|
||||||
}
|
"from_state": from_s,
|
||||||
}, context=to_s.context))
|
"to_state": to_s,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
context=to_s.context,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
matching = check_numeric_state(entity, from_s, to_s)
|
matching = check_numeric_state(entity, from_s, to_s)
|
||||||
|
|
||||||
|
|
@ -86,13 +102,16 @@ def async_trigger(hass, config, action):
|
||||||
|
|
||||||
if time_delta:
|
if time_delta:
|
||||||
unsub_track_same[entity] = async_track_same_state(
|
unsub_track_same[entity] = async_track_same_state(
|
||||||
hass, time_delta, call_action, entity_ids=entity_id,
|
hass,
|
||||||
async_check_same_func=check_numeric_state)
|
time_delta,
|
||||||
|
call_action,
|
||||||
|
entity_ids=entity_id,
|
||||||
|
async_check_same_func=check_numeric_state,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
call_action()
|
call_action()
|
||||||
|
|
||||||
unsub = async_track_state_change(
|
unsub = async_track_state_change(hass, entity_id, state_automation_listener)
|
||||||
hass, entity_id, state_automation_listener)
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_remove():
|
def async_remove():
|
||||||
|
|
|
||||||
|
|
@ -9,22 +9,26 @@ import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.const import MATCH_ALL, CONF_PLATFORM, CONF_FOR
|
from homeassistant.const import MATCH_ALL, CONF_PLATFORM, CONF_FOR
|
||||||
from homeassistant.helpers.event import (
|
from homeassistant.helpers.event import async_track_state_change, async_track_same_state
|
||||||
async_track_state_change, async_track_same_state)
|
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
CONF_ENTITY_ID = 'entity_id'
|
CONF_ENTITY_ID = "entity_id"
|
||||||
CONF_FROM = 'from'
|
CONF_FROM = "from"
|
||||||
CONF_TO = 'to'
|
CONF_TO = "to"
|
||||||
|
|
||||||
TRIGGER_SCHEMA = vol.All(vol.Schema({
|
TRIGGER_SCHEMA = vol.All(
|
||||||
vol.Required(CONF_PLATFORM): 'state',
|
vol.Schema(
|
||||||
vol.Required(CONF_ENTITY_ID): cv.entity_ids,
|
{
|
||||||
# These are str on purpose. Want to catch YAML conversions
|
vol.Required(CONF_PLATFORM): "state",
|
||||||
vol.Optional(CONF_FROM): str,
|
vol.Required(CONF_ENTITY_ID): cv.entity_ids,
|
||||||
vol.Optional(CONF_TO): str,
|
# These are str on purpose. Want to catch YAML conversions
|
||||||
vol.Optional(CONF_FOR): vol.All(cv.time_period, cv.positive_timedelta),
|
vol.Optional(CONF_FROM): str,
|
||||||
}), cv.key_dependency(CONF_FOR, CONF_TO))
|
vol.Optional(CONF_TO): str,
|
||||||
|
vol.Optional(CONF_FOR): vol.All(cv.time_period, cv.positive_timedelta),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
cv.key_dependency(CONF_FOR, CONF_TO),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
|
|
@ -34,28 +38,38 @@ def async_trigger(hass, config, action):
|
||||||
from_state = config.get(CONF_FROM, MATCH_ALL)
|
from_state = config.get(CONF_FROM, MATCH_ALL)
|
||||||
to_state = config.get(CONF_TO, MATCH_ALL)
|
to_state = config.get(CONF_TO, MATCH_ALL)
|
||||||
time_delta = config.get(CONF_FOR)
|
time_delta = config.get(CONF_FOR)
|
||||||
match_all = (from_state == MATCH_ALL and to_state == MATCH_ALL)
|
match_all = from_state == MATCH_ALL and to_state == MATCH_ALL
|
||||||
unsub_track_same = {}
|
unsub_track_same = {}
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def state_automation_listener(entity, from_s, to_s):
|
def state_automation_listener(entity, from_s, to_s):
|
||||||
"""Listen for state changes and calls action."""
|
"""Listen for state changes and calls action."""
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def call_action():
|
def call_action():
|
||||||
"""Call action with right context."""
|
"""Call action with right context."""
|
||||||
hass.async_run_job(action({
|
hass.async_run_job(
|
||||||
'trigger': {
|
action(
|
||||||
'platform': 'state',
|
{
|
||||||
'entity_id': entity,
|
"trigger": {
|
||||||
'from_state': from_s,
|
"platform": "state",
|
||||||
'to_state': to_s,
|
"entity_id": entity,
|
||||||
'for': time_delta,
|
"from_state": from_s,
|
||||||
}
|
"to_state": to_s,
|
||||||
}, context=to_s.context))
|
"for": time_delta,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
context=to_s.context,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# Ignore changes to state attributes if from/to is in use
|
# Ignore changes to state attributes if from/to is in use
|
||||||
if (not match_all and from_s is not None and to_s is not None and
|
if (
|
||||||
from_s.state == to_s.state):
|
not match_all
|
||||||
|
and from_s is not None
|
||||||
|
and to_s is not None
|
||||||
|
and from_s.state == to_s.state
|
||||||
|
):
|
||||||
return
|
return
|
||||||
|
|
||||||
if not time_delta:
|
if not time_delta:
|
||||||
|
|
@ -63,12 +77,16 @@ def async_trigger(hass, config, action):
|
||||||
return
|
return
|
||||||
|
|
||||||
unsub_track_same[entity] = async_track_same_state(
|
unsub_track_same[entity] = async_track_same_state(
|
||||||
hass, time_delta, call_action,
|
hass,
|
||||||
|
time_delta,
|
||||||
|
call_action,
|
||||||
lambda _, _2, to_state: to_state.state == to_s.state,
|
lambda _, _2, to_state: to_state.state == to_s.state,
|
||||||
entity_ids=entity_id)
|
entity_ids=entity_id,
|
||||||
|
)
|
||||||
|
|
||||||
unsub = async_track_state_change(
|
unsub = async_track_state_change(
|
||||||
hass, entity_id, state_automation_listener, from_state, to_state)
|
hass, entity_id, state_automation_listener, from_state, to_state
|
||||||
|
)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_remove():
|
def async_remove():
|
||||||
|
|
|
||||||
|
|
@ -12,17 +12,23 @@ import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_EVENT, CONF_OFFSET, CONF_PLATFORM, SUN_EVENT_SUNRISE)
|
CONF_EVENT,
|
||||||
|
CONF_OFFSET,
|
||||||
|
CONF_PLATFORM,
|
||||||
|
SUN_EVENT_SUNRISE,
|
||||||
|
)
|
||||||
from homeassistant.helpers.event import async_track_sunrise, async_track_sunset
|
from homeassistant.helpers.event import async_track_sunrise, async_track_sunset
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
TRIGGER_SCHEMA = vol.Schema({
|
TRIGGER_SCHEMA = vol.Schema(
|
||||||
vol.Required(CONF_PLATFORM): 'sun',
|
{
|
||||||
vol.Required(CONF_EVENT): cv.sun_event,
|
vol.Required(CONF_PLATFORM): "sun",
|
||||||
vol.Required(CONF_OFFSET, default=timedelta(0)): cv.time_period,
|
vol.Required(CONF_EVENT): cv.sun_event,
|
||||||
})
|
vol.Required(CONF_OFFSET, default=timedelta(0)): cv.time_period,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
|
|
@ -34,13 +40,9 @@ def async_trigger(hass, config, action):
|
||||||
@callback
|
@callback
|
||||||
def call_action():
|
def call_action():
|
||||||
"""Call action with right context."""
|
"""Call action with right context."""
|
||||||
hass.async_run_job(action, {
|
hass.async_run_job(
|
||||||
'trigger': {
|
action, {"trigger": {"platform": "sun", "event": event, "offset": offset}}
|
||||||
'platform': 'sun',
|
)
|
||||||
'event': event,
|
|
||||||
'offset': offset,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
if event == SUN_EVENT_SUNRISE:
|
if event == SUN_EVENT_SUNRISE:
|
||||||
return async_track_sunrise(hass, call_action, offset)
|
return async_track_sunrise(hass, call_action, offset)
|
||||||
|
|
|
||||||
|
|
@ -17,10 +17,12 @@ import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
TRIGGER_SCHEMA = IF_ACTION_SCHEMA = vol.Schema({
|
TRIGGER_SCHEMA = IF_ACTION_SCHEMA = vol.Schema(
|
||||||
vol.Required(CONF_PLATFORM): 'template',
|
{
|
||||||
vol.Required(CONF_VALUE_TEMPLATE): cv.template,
|
vol.Required(CONF_PLATFORM): "template",
|
||||||
})
|
vol.Required(CONF_VALUE_TEMPLATE): cv.template,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
|
|
@ -32,13 +34,18 @@ def async_trigger(hass, config, action):
|
||||||
@callback
|
@callback
|
||||||
def template_listener(entity_id, from_s, to_s):
|
def template_listener(entity_id, from_s, to_s):
|
||||||
"""Listen for state changes and calls action."""
|
"""Listen for state changes and calls action."""
|
||||||
hass.async_run_job(action({
|
hass.async_run_job(
|
||||||
'trigger': {
|
action(
|
||||||
'platform': 'template',
|
{
|
||||||
'entity_id': entity_id,
|
"trigger": {
|
||||||
'from_state': from_s,
|
"platform": "template",
|
||||||
'to_state': to_s,
|
"entity_id": entity_id,
|
||||||
},
|
"from_state": from_s,
|
||||||
}, context=to_s.context))
|
"to_state": to_s,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
context=to_s.context,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
return async_track_template(hass, value_template, template_listener)
|
return async_track_template(hass, value_template, template_listener)
|
||||||
|
|
|
||||||
|
|
@ -14,19 +14,24 @@ from homeassistant.const import CONF_AT, CONF_PLATFORM
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
from homeassistant.helpers.event import async_track_time_change
|
from homeassistant.helpers.event import async_track_time_change
|
||||||
|
|
||||||
CONF_HOURS = 'hours'
|
CONF_HOURS = "hours"
|
||||||
CONF_MINUTES = 'minutes'
|
CONF_MINUTES = "minutes"
|
||||||
CONF_SECONDS = 'seconds'
|
CONF_SECONDS = "seconds"
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
TRIGGER_SCHEMA = vol.All(vol.Schema({
|
TRIGGER_SCHEMA = vol.All(
|
||||||
vol.Required(CONF_PLATFORM): 'time',
|
vol.Schema(
|
||||||
CONF_AT: cv.time,
|
{
|
||||||
CONF_HOURS: vol.Any(vol.Coerce(int), vol.Coerce(str)),
|
vol.Required(CONF_PLATFORM): "time",
|
||||||
CONF_MINUTES: vol.Any(vol.Coerce(int), vol.Coerce(str)),
|
CONF_AT: cv.time,
|
||||||
CONF_SECONDS: vol.Any(vol.Coerce(int), vol.Coerce(str)),
|
CONF_HOURS: vol.Any(vol.Coerce(int), vol.Coerce(str)),
|
||||||
}), cv.has_at_least_one_key(CONF_HOURS, CONF_MINUTES, CONF_SECONDS, CONF_AT))
|
CONF_MINUTES: vol.Any(vol.Coerce(int), vol.Coerce(str)),
|
||||||
|
CONF_SECONDS: vol.Any(vol.Coerce(int), vol.Coerce(str)),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
cv.has_at_least_one_key(CONF_HOURS, CONF_MINUTES, CONF_SECONDS, CONF_AT),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
|
|
@ -43,12 +48,8 @@ def async_trigger(hass, config, action):
|
||||||
@callback
|
@callback
|
||||||
def time_automation_listener(now):
|
def time_automation_listener(now):
|
||||||
"""Listen for time changes and calls action."""
|
"""Listen for time changes and calls action."""
|
||||||
hass.async_run_job(action, {
|
hass.async_run_job(action, {"trigger": {"platform": "time", "now": now}})
|
||||||
'trigger': {
|
|
||||||
'platform': 'time',
|
|
||||||
'now': now,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return async_track_time_change(hass, time_automation_listener,
|
return async_track_time_change(
|
||||||
hour=hours, minute=minutes, second=seconds)
|
hass, time_automation_listener, hour=hours, minute=minutes, second=seconds
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -9,22 +9,29 @@ import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_EVENT, CONF_ENTITY_ID, CONF_ZONE, MATCH_ALL, CONF_PLATFORM)
|
CONF_EVENT,
|
||||||
|
CONF_ENTITY_ID,
|
||||||
|
CONF_ZONE,
|
||||||
|
MATCH_ALL,
|
||||||
|
CONF_PLATFORM,
|
||||||
|
)
|
||||||
from homeassistant.helpers.event import async_track_state_change
|
from homeassistant.helpers.event import async_track_state_change
|
||||||
from homeassistant.helpers import (
|
from homeassistant.helpers import condition, config_validation as cv, location
|
||||||
condition, config_validation as cv, location)
|
|
||||||
|
|
||||||
EVENT_ENTER = 'enter'
|
EVENT_ENTER = "enter"
|
||||||
EVENT_LEAVE = 'leave'
|
EVENT_LEAVE = "leave"
|
||||||
DEFAULT_EVENT = EVENT_ENTER
|
DEFAULT_EVENT = EVENT_ENTER
|
||||||
|
|
||||||
TRIGGER_SCHEMA = vol.Schema({
|
TRIGGER_SCHEMA = vol.Schema(
|
||||||
vol.Required(CONF_PLATFORM): 'zone',
|
{
|
||||||
vol.Required(CONF_ENTITY_ID): cv.entity_ids,
|
vol.Required(CONF_PLATFORM): "zone",
|
||||||
vol.Required(CONF_ZONE): cv.entity_id,
|
vol.Required(CONF_ENTITY_ID): cv.entity_ids,
|
||||||
vol.Required(CONF_EVENT, default=DEFAULT_EVENT):
|
vol.Required(CONF_ZONE): cv.entity_id,
|
||||||
vol.Any(EVENT_ENTER, EVENT_LEAVE),
|
vol.Required(CONF_EVENT, default=DEFAULT_EVENT): vol.Any(
|
||||||
})
|
EVENT_ENTER, EVENT_LEAVE
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
|
|
@ -37,8 +44,11 @@ def async_trigger(hass, config, action):
|
||||||
@callback
|
@callback
|
||||||
def zone_automation_listener(entity, from_s, to_s):
|
def zone_automation_listener(entity, from_s, to_s):
|
||||||
"""Listen for state changes and calls action."""
|
"""Listen for state changes and calls action."""
|
||||||
if from_s and not location.has_location(from_s) or \
|
if (
|
||||||
not location.has_location(to_s):
|
from_s
|
||||||
|
and not location.has_location(from_s)
|
||||||
|
or not location.has_location(to_s)
|
||||||
|
):
|
||||||
return
|
return
|
||||||
|
|
||||||
zone_state = hass.states.get(zone_entity_id)
|
zone_state = hass.states.get(zone_entity_id)
|
||||||
|
|
@ -49,18 +59,30 @@ def async_trigger(hass, config, action):
|
||||||
to_match = condition.zone(hass, zone_state, to_s)
|
to_match = condition.zone(hass, zone_state, to_s)
|
||||||
|
|
||||||
# pylint: disable=too-many-boolean-expressions
|
# pylint: disable=too-many-boolean-expressions
|
||||||
if event == EVENT_ENTER and not from_match and to_match or \
|
if (
|
||||||
event == EVENT_LEAVE and from_match and not to_match:
|
event == EVENT_ENTER
|
||||||
hass.async_run_job(action({
|
and not from_match
|
||||||
'trigger': {
|
and to_match
|
||||||
'platform': 'zone',
|
or event == EVENT_LEAVE
|
||||||
'entity_id': entity,
|
and from_match
|
||||||
'from_state': from_s,
|
and not to_match
|
||||||
'to_state': to_s,
|
):
|
||||||
'zone': zone_state,
|
hass.async_run_job(
|
||||||
'event': event,
|
action(
|
||||||
},
|
{
|
||||||
}, context=to_s.context))
|
"trigger": {
|
||||||
|
"platform": "zone",
|
||||||
|
"entity_id": entity,
|
||||||
|
"from_state": from_s,
|
||||||
|
"to_state": to_s,
|
||||||
|
"zone": zone_state,
|
||||||
|
"event": event,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
context=to_s.context,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
return async_track_state_change(hass, entity_id, zone_automation_listener,
|
return async_track_state_change(
|
||||||
MATCH_ALL, MATCH_ALL)
|
hass, entity_id, zone_automation_listener, MATCH_ALL, MATCH_ALL
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -10,67 +10,76 @@ import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.discovery import SERVICE_AXIS
|
from homeassistant.components.discovery import SERVICE_AXIS
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_LOCATION, ATTR_TRIPPED, CONF_EVENT, CONF_HOST, CONF_INCLUDE,
|
ATTR_LOCATION,
|
||||||
CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_TRIGGER_TIME, CONF_USERNAME,
|
ATTR_TRIPPED,
|
||||||
EVENT_HOMEASSISTANT_STOP)
|
CONF_EVENT,
|
||||||
|
CONF_HOST,
|
||||||
|
CONF_INCLUDE,
|
||||||
|
CONF_NAME,
|
||||||
|
CONF_PASSWORD,
|
||||||
|
CONF_PORT,
|
||||||
|
CONF_TRIGGER_TIME,
|
||||||
|
CONF_USERNAME,
|
||||||
|
EVENT_HOMEASSISTANT_STOP,
|
||||||
|
)
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
from homeassistant.helpers import discovery
|
from homeassistant.helpers import discovery
|
||||||
from homeassistant.helpers.dispatcher import dispatcher_send
|
from homeassistant.helpers.dispatcher import dispatcher_send
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
from homeassistant.util.json import load_json, save_json
|
from homeassistant.util.json import load_json, save_json
|
||||||
|
|
||||||
REQUIREMENTS = ['axis==14']
|
REQUIREMENTS = ["axis==14"]
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DOMAIN = 'axis'
|
DOMAIN = "axis"
|
||||||
CONFIG_FILE = 'axis.conf'
|
CONFIG_FILE = "axis.conf"
|
||||||
|
|
||||||
AXIS_DEVICES = {}
|
AXIS_DEVICES = {}
|
||||||
|
|
||||||
EVENT_TYPES = ['motion', 'vmd3', 'pir', 'sound',
|
EVENT_TYPES = ["motion", "vmd3", "pir", "sound", "daynight", "tampering", "input"]
|
||||||
'daynight', 'tampering', 'input']
|
|
||||||
|
|
||||||
PLATFORMS = ['camera']
|
PLATFORMS = ["camera"]
|
||||||
|
|
||||||
AXIS_INCLUDE = EVENT_TYPES + PLATFORMS
|
AXIS_INCLUDE = EVENT_TYPES + PLATFORMS
|
||||||
|
|
||||||
AXIS_DEFAULT_HOST = '192.168.0.90'
|
AXIS_DEFAULT_HOST = "192.168.0.90"
|
||||||
AXIS_DEFAULT_USERNAME = 'root'
|
AXIS_DEFAULT_USERNAME = "root"
|
||||||
AXIS_DEFAULT_PASSWORD = 'pass'
|
AXIS_DEFAULT_PASSWORD = "pass"
|
||||||
|
|
||||||
DEVICE_SCHEMA = vol.Schema({
|
DEVICE_SCHEMA = vol.Schema(
|
||||||
vol.Required(CONF_INCLUDE):
|
{
|
||||||
vol.All(cv.ensure_list, [vol.In(AXIS_INCLUDE)]),
|
vol.Required(CONF_INCLUDE): vol.All(cv.ensure_list, [vol.In(AXIS_INCLUDE)]),
|
||||||
vol.Optional(CONF_NAME): cv.string,
|
vol.Optional(CONF_NAME): cv.string,
|
||||||
vol.Optional(CONF_HOST, default=AXIS_DEFAULT_HOST): cv.string,
|
vol.Optional(CONF_HOST, default=AXIS_DEFAULT_HOST): cv.string,
|
||||||
vol.Optional(CONF_USERNAME, default=AXIS_DEFAULT_USERNAME): cv.string,
|
vol.Optional(CONF_USERNAME, default=AXIS_DEFAULT_USERNAME): cv.string,
|
||||||
vol.Optional(CONF_PASSWORD, default=AXIS_DEFAULT_PASSWORD): cv.string,
|
vol.Optional(CONF_PASSWORD, default=AXIS_DEFAULT_PASSWORD): cv.string,
|
||||||
vol.Optional(CONF_TRIGGER_TIME, default=0): cv.positive_int,
|
vol.Optional(CONF_TRIGGER_TIME, default=0): cv.positive_int,
|
||||||
vol.Optional(CONF_PORT, default=80): cv.positive_int,
|
vol.Optional(CONF_PORT, default=80): cv.positive_int,
|
||||||
vol.Optional(ATTR_LOCATION, default=''): cv.string,
|
vol.Optional(ATTR_LOCATION, default=""): cv.string,
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema({
|
CONFIG_SCHEMA = vol.Schema(
|
||||||
DOMAIN: vol.Schema({
|
{DOMAIN: vol.Schema({cv.slug: DEVICE_SCHEMA})}, extra=vol.ALLOW_EXTRA
|
||||||
cv.slug: DEVICE_SCHEMA,
|
)
|
||||||
}),
|
|
||||||
}, extra=vol.ALLOW_EXTRA)
|
|
||||||
|
|
||||||
SERVICE_VAPIX_CALL = 'vapix_call'
|
SERVICE_VAPIX_CALL = "vapix_call"
|
||||||
SERVICE_VAPIX_CALL_RESPONSE = 'vapix_call_response'
|
SERVICE_VAPIX_CALL_RESPONSE = "vapix_call_response"
|
||||||
SERVICE_CGI = 'cgi'
|
SERVICE_CGI = "cgi"
|
||||||
SERVICE_ACTION = 'action'
|
SERVICE_ACTION = "action"
|
||||||
SERVICE_PARAM = 'param'
|
SERVICE_PARAM = "param"
|
||||||
SERVICE_DEFAULT_CGI = 'param.cgi'
|
SERVICE_DEFAULT_CGI = "param.cgi"
|
||||||
SERVICE_DEFAULT_ACTION = 'update'
|
SERVICE_DEFAULT_ACTION = "update"
|
||||||
|
|
||||||
SERVICE_SCHEMA = vol.Schema({
|
SERVICE_SCHEMA = vol.Schema(
|
||||||
vol.Required(CONF_NAME): cv.string,
|
{
|
||||||
vol.Required(SERVICE_PARAM): cv.string,
|
vol.Required(CONF_NAME): cv.string,
|
||||||
vol.Optional(SERVICE_CGI, default=SERVICE_DEFAULT_CGI): cv.string,
|
vol.Required(SERVICE_PARAM): cv.string,
|
||||||
vol.Optional(SERVICE_ACTION, default=SERVICE_DEFAULT_ACTION): cv.string,
|
vol.Optional(SERVICE_CGI, default=SERVICE_DEFAULT_CGI): cv.string,
|
||||||
})
|
vol.Optional(SERVICE_ACTION, default=SERVICE_DEFAULT_ACTION): cv.string,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def request_configuration(hass, config, name, host, serialnumber):
|
def request_configuration(hass, config, name, host, serialnumber):
|
||||||
|
|
@ -80,8 +89,7 @@ def request_configuration(hass, config, name, host, serialnumber):
|
||||||
def configuration_callback(callback_data):
|
def configuration_callback(callback_data):
|
||||||
"""Call when configuration is submitted."""
|
"""Call when configuration is submitted."""
|
||||||
if CONF_INCLUDE not in callback_data:
|
if CONF_INCLUDE not in callback_data:
|
||||||
configurator.notify_errors(
|
configurator.notify_errors(request_id, "Functionality mandatory.")
|
||||||
request_id, "Functionality mandatory.")
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
callback_data[CONF_INCLUDE] = callback_data[CONF_INCLUDE].split()
|
callback_data[CONF_INCLUDE] = callback_data[CONF_INCLUDE].split()
|
||||||
|
|
@ -93,58 +101,58 @@ def request_configuration(hass, config, name, host, serialnumber):
|
||||||
try:
|
try:
|
||||||
device_config = DEVICE_SCHEMA(callback_data)
|
device_config = DEVICE_SCHEMA(callback_data)
|
||||||
except vol.Invalid:
|
except vol.Invalid:
|
||||||
configurator.notify_errors(
|
configurator.notify_errors(request_id, "Bad input, please check spelling.")
|
||||||
request_id, "Bad input, please check spelling.")
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if setup_device(hass, config, device_config):
|
if setup_device(hass, config, device_config):
|
||||||
del device_config['events']
|
del device_config["events"]
|
||||||
del device_config['signal']
|
del device_config["signal"]
|
||||||
config_file = load_json(hass.config.path(CONFIG_FILE))
|
config_file = load_json(hass.config.path(CONFIG_FILE))
|
||||||
config_file[serialnumber] = dict(device_config)
|
config_file[serialnumber] = dict(device_config)
|
||||||
save_json(hass.config.path(CONFIG_FILE), config_file)
|
save_json(hass.config.path(CONFIG_FILE), config_file)
|
||||||
configurator.request_done(request_id)
|
configurator.request_done(request_id)
|
||||||
else:
|
else:
|
||||||
configurator.notify_errors(
|
configurator.notify_errors(
|
||||||
request_id, "Failed to register, please try again.")
|
request_id, "Failed to register, please try again."
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
title = '{} ({})'.format(name, host)
|
title = "{} ({})".format(name, host)
|
||||||
request_id = configurator.request_config(
|
request_id = configurator.request_config(
|
||||||
title, configuration_callback,
|
title,
|
||||||
description='Functionality: ' + str(AXIS_INCLUDE),
|
configuration_callback,
|
||||||
|
description="Functionality: " + str(AXIS_INCLUDE),
|
||||||
entity_picture="/static/images/logo_axis.png",
|
entity_picture="/static/images/logo_axis.png",
|
||||||
link_name='Axis platform documentation',
|
link_name="Axis platform documentation",
|
||||||
link_url='https://home-assistant.io/components/axis/',
|
link_url="https://home-assistant.io/components/axis/",
|
||||||
submit_caption="Confirm",
|
submit_caption="Confirm",
|
||||||
fields=[
|
fields=[
|
||||||
{'id': CONF_NAME,
|
{"id": CONF_NAME, "name": "Device name", "type": "text"},
|
||||||
'name': "Device name",
|
{"id": CONF_USERNAME, "name": "User name", "type": "text"},
|
||||||
'type': 'text'},
|
{"id": CONF_PASSWORD, "name": "Password", "type": "password"},
|
||||||
{'id': CONF_USERNAME,
|
{
|
||||||
'name': "User name",
|
"id": CONF_INCLUDE,
|
||||||
'type': 'text'},
|
"name": "Device functionality (space separated list)",
|
||||||
{'id': CONF_PASSWORD,
|
"type": "text",
|
||||||
'name': 'Password',
|
},
|
||||||
'type': 'password'},
|
{
|
||||||
{'id': CONF_INCLUDE,
|
"id": ATTR_LOCATION,
|
||||||
'name': "Device functionality (space separated list)",
|
"name": "Physical location of device (optional)",
|
||||||
'type': 'text'},
|
"type": "text",
|
||||||
{'id': ATTR_LOCATION,
|
},
|
||||||
'name': "Physical location of device (optional)",
|
{"id": CONF_PORT, "name": "HTTP port (default=80)", "type": "number"},
|
||||||
'type': 'text'},
|
{
|
||||||
{'id': CONF_PORT,
|
"id": CONF_TRIGGER_TIME,
|
||||||
'name': "HTTP port (default=80)",
|
"name": "Sensor update interval (optional)",
|
||||||
'type': 'number'},
|
"type": "number",
|
||||||
{'id': CONF_TRIGGER_TIME,
|
},
|
||||||
'name': "Sensor update interval (optional)",
|
],
|
||||||
'type': 'number'},
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def setup(hass, config):
|
def setup(hass, config):
|
||||||
"""Set up for Axis devices."""
|
"""Set up for Axis devices."""
|
||||||
|
|
||||||
def _shutdown(call):
|
def _shutdown(call):
|
||||||
"""Stop the event stream on shutdown."""
|
"""Stop the event stream on shutdown."""
|
||||||
for serialnumber, device in AXIS_DEVICES.items():
|
for serialnumber, device in AXIS_DEVICES.items():
|
||||||
|
|
@ -156,8 +164,8 @@ def setup(hass, config):
|
||||||
def axis_device_discovered(service, discovery_info):
|
def axis_device_discovered(service, discovery_info):
|
||||||
"""Call when axis devices has been found."""
|
"""Call when axis devices has been found."""
|
||||||
host = discovery_info[CONF_HOST]
|
host = discovery_info[CONF_HOST]
|
||||||
name = discovery_info['hostname']
|
name = discovery_info["hostname"]
|
||||||
serialnumber = discovery_info['properties']['macaddress']
|
serialnumber = discovery_info["properties"]["macaddress"]
|
||||||
|
|
||||||
if serialnumber not in AXIS_DEVICES:
|
if serialnumber not in AXIS_DEVICES:
|
||||||
config_file = load_json(hass.config.path(CONFIG_FILE))
|
config_file = load_json(hass.config.path(CONFIG_FILE))
|
||||||
|
|
@ -170,8 +178,7 @@ def setup(hass, config):
|
||||||
_LOGGER.error("Bad data from %s. %s", CONFIG_FILE, err)
|
_LOGGER.error("Bad data from %s. %s", CONFIG_FILE, err)
|
||||||
return False
|
return False
|
||||||
if not setup_device(hass, config, device_config):
|
if not setup_device(hass, config, device_config):
|
||||||
_LOGGER.error(
|
_LOGGER.error("Couldn't set up %s", device_config[CONF_NAME])
|
||||||
"Couldn't set up %s", device_config[CONF_NAME])
|
|
||||||
else:
|
else:
|
||||||
# New device, create configuration request for UI
|
# New device, create configuration request for UI
|
||||||
request_configuration(hass, config, name, host, serialnumber)
|
request_configuration(hass, config, name, host, serialnumber)
|
||||||
|
|
@ -179,7 +186,7 @@ def setup(hass, config):
|
||||||
# Device already registered, but on a different IP
|
# Device already registered, but on a different IP
|
||||||
device = AXIS_DEVICES[serialnumber]
|
device = AXIS_DEVICES[serialnumber]
|
||||||
device.config.host = host
|
device.config.host = host
|
||||||
dispatcher_send(hass, DOMAIN + '_' + device.name + '_new_ip', host)
|
dispatcher_send(hass, DOMAIN + "_" + device.name + "_new_ip", host)
|
||||||
|
|
||||||
# Register discovery service
|
# Register discovery service
|
||||||
discovery.listen(hass, SERVICE_AXIS, axis_device_discovered)
|
discovery.listen(hass, SERVICE_AXIS, axis_device_discovered)
|
||||||
|
|
@ -199,7 +206,8 @@ def setup(hass, config):
|
||||||
response = device.vapix.do_request(
|
response = device.vapix.do_request(
|
||||||
call.data[SERVICE_CGI],
|
call.data[SERVICE_CGI],
|
||||||
call.data[SERVICE_ACTION],
|
call.data[SERVICE_ACTION],
|
||||||
call.data[SERVICE_PARAM])
|
call.data[SERVICE_PARAM],
|
||||||
|
)
|
||||||
hass.bus.fire(SERVICE_VAPIX_CALL_RESPONSE, response)
|
hass.bus.fire(SERVICE_VAPIX_CALL_RESPONSE, response)
|
||||||
return True
|
return True
|
||||||
_LOGGER.info("Couldn't find device %s", call.data[CONF_NAME])
|
_LOGGER.info("Couldn't find device %s", call.data[CONF_NAME])
|
||||||
|
|
@ -207,7 +215,8 @@ def setup(hass, config):
|
||||||
|
|
||||||
# Register service with Home Assistant.
|
# Register service with Home Assistant.
|
||||||
hass.services.register(
|
hass.services.register(
|
||||||
DOMAIN, SERVICE_VAPIX_CALL, vapix_service, schema=SERVICE_SCHEMA)
|
DOMAIN, SERVICE_VAPIX_CALL, vapix_service, schema=SERVICE_SCHEMA
|
||||||
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -217,21 +226,19 @@ def setup_device(hass, config, device_config):
|
||||||
|
|
||||||
def signal_callback(action, event):
|
def signal_callback(action, event):
|
||||||
"""Call to configure events when initialized on event stream."""
|
"""Call to configure events when initialized on event stream."""
|
||||||
if action == 'add':
|
if action == "add":
|
||||||
event_config = {
|
event_config = {
|
||||||
CONF_EVENT: event,
|
CONF_EVENT: event,
|
||||||
CONF_NAME: device_config[CONF_NAME],
|
CONF_NAME: device_config[CONF_NAME],
|
||||||
ATTR_LOCATION: device_config[ATTR_LOCATION],
|
ATTR_LOCATION: device_config[ATTR_LOCATION],
|
||||||
CONF_TRIGGER_TIME: device_config[CONF_TRIGGER_TIME]
|
CONF_TRIGGER_TIME: device_config[CONF_TRIGGER_TIME],
|
||||||
}
|
}
|
||||||
component = event.event_platform
|
component = event.event_platform
|
||||||
discovery.load_platform(
|
discovery.load_platform(hass, component, DOMAIN, event_config, config)
|
||||||
hass, component, DOMAIN, event_config, config)
|
|
||||||
|
|
||||||
event_types = list(filter(lambda x: x in device_config[CONF_INCLUDE],
|
event_types = list(filter(lambda x: x in device_config[CONF_INCLUDE], EVENT_TYPES))
|
||||||
EVENT_TYPES))
|
device_config["events"] = event_types
|
||||||
device_config['events'] = event_types
|
device_config["signal"] = signal_callback
|
||||||
device_config['signal'] = signal_callback
|
|
||||||
device = AxisDevice(hass.loop, **device_config)
|
device = AxisDevice(hass.loop, **device_config)
|
||||||
device.name = device_config[CONF_NAME]
|
device.name = device_config[CONF_NAME]
|
||||||
|
|
||||||
|
|
@ -241,16 +248,15 @@ def setup_device(hass, config, device_config):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
for component in device_config[CONF_INCLUDE]:
|
for component in device_config[CONF_INCLUDE]:
|
||||||
if component == 'camera':
|
if component == "camera":
|
||||||
camera_config = {
|
camera_config = {
|
||||||
CONF_NAME: device_config[CONF_NAME],
|
CONF_NAME: device_config[CONF_NAME],
|
||||||
CONF_HOST: device_config[CONF_HOST],
|
CONF_HOST: device_config[CONF_HOST],
|
||||||
CONF_PORT: device_config[CONF_PORT],
|
CONF_PORT: device_config[CONF_PORT],
|
||||||
CONF_USERNAME: device_config[CONF_USERNAME],
|
CONF_USERNAME: device_config[CONF_USERNAME],
|
||||||
CONF_PASSWORD: device_config[CONF_PASSWORD]
|
CONF_PASSWORD: device_config[CONF_PASSWORD],
|
||||||
}
|
}
|
||||||
discovery.load_platform(
|
discovery.load_platform(hass, component, DOMAIN, camera_config, config)
|
||||||
hass, component, DOMAIN, camera_config, config)
|
|
||||||
|
|
||||||
AXIS_DEVICES[device.serial_number] = device
|
AXIS_DEVICES[device.serial_number] = device
|
||||||
if event_types:
|
if event_types:
|
||||||
|
|
@ -264,9 +270,9 @@ class AxisDeviceEvent(Entity):
|
||||||
def __init__(self, event_config):
|
def __init__(self, event_config):
|
||||||
"""Initialize the event."""
|
"""Initialize the event."""
|
||||||
self.axis_event = event_config[CONF_EVENT]
|
self.axis_event = event_config[CONF_EVENT]
|
||||||
self._name = '{}_{}_{}'.format(
|
self._name = "{}_{}_{}".format(
|
||||||
event_config[CONF_NAME], self.axis_event.event_type,
|
event_config[CONF_NAME], self.axis_event.event_type, self.axis_event.id
|
||||||
self.axis_event.id)
|
)
|
||||||
self.location = event_config[ATTR_LOCATION]
|
self.location = event_config[ATTR_LOCATION]
|
||||||
self.axis_event.callback = self._update_callback
|
self.axis_event.callback = self._update_callback
|
||||||
|
|
||||||
|
|
@ -295,7 +301,7 @@ class AxisDeviceEvent(Entity):
|
||||||
attr = {}
|
attr = {}
|
||||||
|
|
||||||
tripped = self.axis_event.is_tripped
|
tripped = self.axis_event.is_tripped
|
||||||
attr[ATTR_TRIPPED] = 'True' if tripped else 'False'
|
attr[ATTR_TRIPPED] = "True" if tripped else "False"
|
||||||
|
|
||||||
attr[ATTR_LOCATION] = self.location
|
attr[ATTR_LOCATION] = self.location
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,14 +6,13 @@ https://home-assistant.io/components/bbb_gpio/
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP
|
||||||
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
|
|
||||||
|
|
||||||
REQUIREMENTS = ['Adafruit_BBIO==1.0.0']
|
REQUIREMENTS = ["Adafruit_BBIO==1.0.0"]
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DOMAIN = 'bbb_gpio'
|
DOMAIN = "bbb_gpio"
|
||||||
|
|
||||||
|
|
||||||
def setup(hass, config):
|
def setup(hass, config):
|
||||||
|
|
@ -37,6 +36,7 @@ def setup_output(pin):
|
||||||
"""Set up a GPIO as output."""
|
"""Set up a GPIO as output."""
|
||||||
# pylint: disable=import-error
|
# pylint: disable=import-error
|
||||||
from Adafruit_BBIO import GPIO
|
from Adafruit_BBIO import GPIO
|
||||||
|
|
||||||
GPIO.setup(pin, GPIO.OUT)
|
GPIO.setup(pin, GPIO.OUT)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -44,15 +44,15 @@ def setup_input(pin, pull_mode):
|
||||||
"""Set up a GPIO as input."""
|
"""Set up a GPIO as input."""
|
||||||
# pylint: disable=import-error
|
# pylint: disable=import-error
|
||||||
from Adafruit_BBIO import GPIO
|
from Adafruit_BBIO import GPIO
|
||||||
GPIO.setup(pin, GPIO.IN,
|
|
||||||
GPIO.PUD_DOWN if pull_mode == 'DOWN'
|
GPIO.setup(pin, GPIO.IN, GPIO.PUD_DOWN if pull_mode == "DOWN" else GPIO.PUD_UP)
|
||||||
else GPIO.PUD_UP)
|
|
||||||
|
|
||||||
|
|
||||||
def write_output(pin, value):
|
def write_output(pin, value):
|
||||||
"""Write a value to a GPIO."""
|
"""Write a value to a GPIO."""
|
||||||
# pylint: disable=import-error
|
# pylint: disable=import-error
|
||||||
from Adafruit_BBIO import GPIO
|
from Adafruit_BBIO import GPIO
|
||||||
|
|
||||||
GPIO.output(pin, value)
|
GPIO.output(pin, value)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -60,6 +60,7 @@ def read_input(pin):
|
||||||
"""Read a value from a GPIO."""
|
"""Read a value from a GPIO."""
|
||||||
# pylint: disable=import-error
|
# pylint: disable=import-error
|
||||||
from Adafruit_BBIO import GPIO
|
from Adafruit_BBIO import GPIO
|
||||||
|
|
||||||
return GPIO.input(pin) is GPIO.HIGH
|
return GPIO.input(pin) is GPIO.HIGH
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -67,5 +68,5 @@ def edge_detect(pin, event_callback, bounce):
|
||||||
"""Add detection for RISING and FALLING events."""
|
"""Add detection for RISING and FALLING events."""
|
||||||
# pylint: disable=import-error
|
# pylint: disable=import-error
|
||||||
from Adafruit_BBIO import GPIO
|
from Adafruit_BBIO import GPIO
|
||||||
GPIO.add_event_detect(
|
|
||||||
pin, GPIO.BOTH, callback=event_callback, bouncetime=bounce)
|
GPIO.add_event_detect(pin, GPIO.BOTH, callback=event_callback, bouncetime=bounce)
|
||||||
|
|
|
||||||
|
|
@ -12,37 +12,37 @@ import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.helpers.entity_component import EntityComponent
|
from homeassistant.helpers.entity_component import EntityComponent
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
from homeassistant.const import (STATE_ON, STATE_OFF)
|
from homeassistant.const import STATE_ON, STATE_OFF
|
||||||
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
||||||
|
|
||||||
DOMAIN = 'binary_sensor'
|
DOMAIN = "binary_sensor"
|
||||||
SCAN_INTERVAL = timedelta(seconds=30)
|
SCAN_INTERVAL = timedelta(seconds=30)
|
||||||
|
|
||||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
ENTITY_ID_FORMAT = DOMAIN + ".{}"
|
||||||
DEVICE_CLASSES = [
|
DEVICE_CLASSES = [
|
||||||
'battery', # On means low, Off means normal
|
"battery", # On means low, Off means normal
|
||||||
'cold', # On means cold, Off means normal
|
"cold", # On means cold, Off means normal
|
||||||
'connectivity', # On means connected, Off means disconnected
|
"connectivity", # On means connected, Off means disconnected
|
||||||
'door', # On means open, Off means closed
|
"door", # On means open, Off means closed
|
||||||
'garage_door', # On means open, Off means closed
|
"garage_door", # On means open, Off means closed
|
||||||
'gas', # On means gas detected, Off means no gas (clear)
|
"gas", # On means gas detected, Off means no gas (clear)
|
||||||
'heat', # On means hot, Off means normal
|
"heat", # On means hot, Off means normal
|
||||||
'light', # On means light detected, Off means no light
|
"light", # On means light detected, Off means no light
|
||||||
'lock', # On means open (unlocked), Off means closed (locked)
|
"lock", # On means open (unlocked), Off means closed (locked)
|
||||||
'moisture', # On means wet, Off means dry
|
"moisture", # On means wet, Off means dry
|
||||||
'motion', # On means motion detected, Off means no motion (clear)
|
"motion", # On means motion detected, Off means no motion (clear)
|
||||||
'moving', # On means moving, Off means not moving (stopped)
|
"moving", # On means moving, Off means not moving (stopped)
|
||||||
'occupancy', # On means occupied, Off means not occupied (clear)
|
"occupancy", # On means occupied, Off means not occupied (clear)
|
||||||
'opening', # On means open, Off means closed
|
"opening", # On means open, Off means closed
|
||||||
'plug', # On means plugged in, Off means unplugged
|
"plug", # On means plugged in, Off means unplugged
|
||||||
'power', # On means power detected, Off means no power
|
"power", # On means power detected, Off means no power
|
||||||
'presence', # On means home, Off means away
|
"presence", # On means home, Off means away
|
||||||
'problem', # On means problem detected, Off means no problem (OK)
|
"problem", # On means problem detected, Off means no problem (OK)
|
||||||
'safety', # On means unsafe, Off means safe
|
"safety", # On means unsafe, Off means safe
|
||||||
'smoke', # On means smoke detected, Off means no smoke (clear)
|
"smoke", # On means smoke detected, Off means no smoke (clear)
|
||||||
'sound', # On means sound detected, Off means no sound (clear)
|
"sound", # On means sound detected, Off means no sound (clear)
|
||||||
'vibration', # On means vibration detected, Off means no vibration
|
"vibration", # On means vibration detected, Off means no vibration
|
||||||
'window', # On means open, Off means closed
|
"window", # On means open, Off means closed
|
||||||
]
|
]
|
||||||
|
|
||||||
DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES))
|
DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES))
|
||||||
|
|
@ -51,7 +51,8 @@ DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES))
|
||||||
async def async_setup(hass, config):
|
async def async_setup(hass, config):
|
||||||
"""Track states and offer events for binary sensors."""
|
"""Track states and offer events for binary sensors."""
|
||||||
component = hass.data[DOMAIN] = EntityComponent(
|
component = hass.data[DOMAIN] = EntityComponent(
|
||||||
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
|
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL
|
||||||
|
)
|
||||||
|
|
||||||
await component.async_setup(config)
|
await component.async_setup(config)
|
||||||
return True
|
return True
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,15 @@ https://home-assistant.io/components/binary_sensor.abode/
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from homeassistant.components.abode import (AbodeDevice, AbodeAutomation,
|
from homeassistant.components.abode import (
|
||||||
DOMAIN as ABODE_DOMAIN)
|
AbodeDevice,
|
||||||
|
AbodeAutomation,
|
||||||
|
DOMAIN as ABODE_DOMAIN,
|
||||||
|
)
|
||||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||||
|
|
||||||
|
|
||||||
DEPENDENCIES = ['abode']
|
DEPENDENCIES = ["abode"]
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -23,9 +26,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
|
|
||||||
data = hass.data[ABODE_DOMAIN]
|
data = hass.data[ABODE_DOMAIN]
|
||||||
|
|
||||||
device_types = [CONST.TYPE_CONNECTIVITY, CONST.TYPE_MOISTURE,
|
device_types = [
|
||||||
CONST.TYPE_MOTION, CONST.TYPE_OCCUPANCY,
|
CONST.TYPE_CONNECTIVITY,
|
||||||
CONST.TYPE_OPENING]
|
CONST.TYPE_MOISTURE,
|
||||||
|
CONST.TYPE_MOTION,
|
||||||
|
CONST.TYPE_OCCUPANCY,
|
||||||
|
CONST.TYPE_OPENING,
|
||||||
|
]
|
||||||
|
|
||||||
devices = []
|
devices = []
|
||||||
for device in data.abode.get_devices(generic_type=device_types):
|
for device in data.abode.get_devices(generic_type=device_types):
|
||||||
|
|
@ -34,13 +41,15 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
|
|
||||||
devices.append(AbodeBinarySensor(data, device))
|
devices.append(AbodeBinarySensor(data, device))
|
||||||
|
|
||||||
for automation in data.abode.get_automations(
|
for automation in data.abode.get_automations(generic_type=CONST.TYPE_QUICK_ACTION):
|
||||||
generic_type=CONST.TYPE_QUICK_ACTION):
|
|
||||||
if data.is_automation_excluded(automation):
|
if data.is_automation_excluded(automation):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
devices.append(AbodeQuickActionBinarySensor(
|
devices.append(
|
||||||
data, automation, TIMELINE.AUTOMATION_EDIT_GROUP))
|
AbodeQuickActionBinarySensor(
|
||||||
|
data, automation, TIMELINE.AUTOMATION_EDIT_GROUP
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
data.devices.extend(devices)
|
data.devices.extend(devices)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,20 +11,25 @@ import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.ads import CONF_ADS_VAR, DATA_ADS
|
from homeassistant.components.ads import CONF_ADS_VAR, DATA_ADS
|
||||||
from homeassistant.components.binary_sensor import (
|
from homeassistant.components.binary_sensor import (
|
||||||
DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA, BinarySensorDevice)
|
DEVICE_CLASSES_SCHEMA,
|
||||||
|
PLATFORM_SCHEMA,
|
||||||
|
BinarySensorDevice,
|
||||||
|
)
|
||||||
from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME
|
from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DEFAULT_NAME = 'ADS binary sensor'
|
DEFAULT_NAME = "ADS binary sensor"
|
||||||
DEPENDENCIES = ['ads']
|
DEPENDENCIES = ["ads"]
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||||
vol.Required(CONF_ADS_VAR): cv.string,
|
{
|
||||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
vol.Required(CONF_ADS_VAR): cv.string,
|
||||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||||
})
|
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
|
|
@ -46,22 +51,26 @@ class AdsBinarySensor(BinarySensorDevice):
|
||||||
"""Initialize ADS binary sensor."""
|
"""Initialize ADS binary sensor."""
|
||||||
self._name = name
|
self._name = name
|
||||||
self._state = False
|
self._state = False
|
||||||
self._device_class = device_class or 'moving'
|
self._device_class = device_class or "moving"
|
||||||
self._ads_hub = ads_hub
|
self._ads_hub = ads_hub
|
||||||
self.ads_var = ads_var
|
self.ads_var = ads_var
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_added_to_hass(self):
|
def async_added_to_hass(self):
|
||||||
"""Register device notification."""
|
"""Register device notification."""
|
||||||
|
|
||||||
def update(name, value):
|
def update(name, value):
|
||||||
"""Handle device notifications."""
|
"""Handle device notifications."""
|
||||||
_LOGGER.debug('Variable %s changed its value to %d', name, value)
|
_LOGGER.debug("Variable %s changed its value to %d", name, value)
|
||||||
self._state = value
|
self._state = value
|
||||||
self.schedule_update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
self.hass.async_add_job(
|
self.hass.async_add_job(
|
||||||
self._ads_hub.add_device_notification,
|
self._ads_hub.add_device_notification,
|
||||||
self.ads_var, self._ads_hub.PLCTYPE_BOOL, update)
|
self.ads_var,
|
||||||
|
self._ads_hub.PLCTYPE_BOOL,
|
||||||
|
update,
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
|
|
|
||||||
|
|
@ -9,23 +9,31 @@ import logging
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||||
from homeassistant.components.alarmdecoder import (
|
from homeassistant.components.alarmdecoder import (
|
||||||
ZONE_SCHEMA, CONF_ZONES, CONF_ZONE_NAME, CONF_ZONE_TYPE,
|
ZONE_SCHEMA,
|
||||||
CONF_ZONE_RFID, SIGNAL_ZONE_FAULT, SIGNAL_ZONE_RESTORE,
|
CONF_ZONES,
|
||||||
SIGNAL_RFX_MESSAGE, SIGNAL_REL_MESSAGE, CONF_RELAY_ADDR,
|
CONF_ZONE_NAME,
|
||||||
CONF_RELAY_CHAN)
|
CONF_ZONE_TYPE,
|
||||||
|
CONF_ZONE_RFID,
|
||||||
|
SIGNAL_ZONE_FAULT,
|
||||||
|
SIGNAL_ZONE_RESTORE,
|
||||||
|
SIGNAL_RFX_MESSAGE,
|
||||||
|
SIGNAL_REL_MESSAGE,
|
||||||
|
CONF_RELAY_ADDR,
|
||||||
|
CONF_RELAY_CHAN,
|
||||||
|
)
|
||||||
|
|
||||||
DEPENDENCIES = ['alarmdecoder']
|
DEPENDENCIES = ["alarmdecoder"]
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
ATTR_RF_BIT0 = 'rf_bit0'
|
ATTR_RF_BIT0 = "rf_bit0"
|
||||||
ATTR_RF_LOW_BAT = 'rf_low_battery'
|
ATTR_RF_LOW_BAT = "rf_low_battery"
|
||||||
ATTR_RF_SUPERVISED = 'rf_supervised'
|
ATTR_RF_SUPERVISED = "rf_supervised"
|
||||||
ATTR_RF_BIT3 = 'rf_bit3'
|
ATTR_RF_BIT3 = "rf_bit3"
|
||||||
ATTR_RF_LOOP3 = 'rf_loop3'
|
ATTR_RF_LOOP3 = "rf_loop3"
|
||||||
ATTR_RF_LOOP2 = 'rf_loop2'
|
ATTR_RF_LOOP2 = "rf_loop2"
|
||||||
ATTR_RF_LOOP4 = 'rf_loop4'
|
ATTR_RF_LOOP4 = "rf_loop4"
|
||||||
ATTR_RF_LOOP1 = 'rf_loop1'
|
ATTR_RF_LOOP1 = "rf_loop1"
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
|
|
@ -41,7 +49,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
relay_addr = device_config_data.get(CONF_RELAY_ADDR)
|
relay_addr = device_config_data.get(CONF_RELAY_ADDR)
|
||||||
relay_chan = device_config_data.get(CONF_RELAY_CHAN)
|
relay_chan = device_config_data.get(CONF_RELAY_CHAN)
|
||||||
device = AlarmDecoderBinarySensor(
|
device = AlarmDecoderBinarySensor(
|
||||||
zone_num, zone_name, zone_type, zone_rfid, relay_addr, relay_chan)
|
zone_num, zone_name, zone_type, zone_rfid, relay_addr, relay_chan
|
||||||
|
)
|
||||||
devices.append(device)
|
devices.append(device)
|
||||||
|
|
||||||
add_entities(devices)
|
add_entities(devices)
|
||||||
|
|
@ -52,8 +61,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
class AlarmDecoderBinarySensor(BinarySensorDevice):
|
class AlarmDecoderBinarySensor(BinarySensorDevice):
|
||||||
"""Representation of an AlarmDecoder binary sensor."""
|
"""Representation of an AlarmDecoder binary sensor."""
|
||||||
|
|
||||||
def __init__(self, zone_number, zone_name, zone_type, zone_rfid,
|
def __init__(
|
||||||
relay_addr, relay_chan):
|
self, zone_number, zone_name, zone_type, zone_rfid, relay_addr, relay_chan
|
||||||
|
):
|
||||||
"""Initialize the binary_sensor."""
|
"""Initialize the binary_sensor."""
|
||||||
self._zone_number = zone_number
|
self._zone_number = zone_number
|
||||||
self._zone_type = zone_type
|
self._zone_type = zone_type
|
||||||
|
|
@ -68,16 +78,20 @@ class AlarmDecoderBinarySensor(BinarySensorDevice):
|
||||||
def async_added_to_hass(self):
|
def async_added_to_hass(self):
|
||||||
"""Register callbacks."""
|
"""Register callbacks."""
|
||||||
self.hass.helpers.dispatcher.async_dispatcher_connect(
|
self.hass.helpers.dispatcher.async_dispatcher_connect(
|
||||||
SIGNAL_ZONE_FAULT, self._fault_callback)
|
SIGNAL_ZONE_FAULT, self._fault_callback
|
||||||
|
)
|
||||||
|
|
||||||
self.hass.helpers.dispatcher.async_dispatcher_connect(
|
self.hass.helpers.dispatcher.async_dispatcher_connect(
|
||||||
SIGNAL_ZONE_RESTORE, self._restore_callback)
|
SIGNAL_ZONE_RESTORE, self._restore_callback
|
||||||
|
)
|
||||||
|
|
||||||
self.hass.helpers.dispatcher.async_dispatcher_connect(
|
self.hass.helpers.dispatcher.async_dispatcher_connect(
|
||||||
SIGNAL_RFX_MESSAGE, self._rfx_message_callback)
|
SIGNAL_RFX_MESSAGE, self._rfx_message_callback
|
||||||
|
)
|
||||||
|
|
||||||
self.hass.helpers.dispatcher.async_dispatcher_connect(
|
self.hass.helpers.dispatcher.async_dispatcher_connect(
|
||||||
SIGNAL_REL_MESSAGE, self._rel_message_callback)
|
SIGNAL_REL_MESSAGE, self._rel_message_callback
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
|
|
@ -134,9 +148,9 @@ class AlarmDecoderBinarySensor(BinarySensorDevice):
|
||||||
|
|
||||||
def _rel_message_callback(self, message):
|
def _rel_message_callback(self, message):
|
||||||
"""Update relay state."""
|
"""Update relay state."""
|
||||||
if (self._relay_addr == message.address and
|
if self._relay_addr == message.address and self._relay_chan == message.channel:
|
||||||
self._relay_chan == message.channel):
|
_LOGGER.debug(
|
||||||
_LOGGER.debug("Relay %d:%d value:%d", message.address,
|
"Relay %d:%d value:%d", message.address, message.channel, message.value
|
||||||
message.channel, message.value)
|
)
|
||||||
self._state = message.value
|
self._state = message.value
|
||||||
self.schedule_update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
|
||||||
|
|
@ -8,14 +8,18 @@ import asyncio
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||||
from homeassistant.components.android_ip_webcam import (
|
from homeassistant.components.android_ip_webcam import (
|
||||||
KEY_MAP, DATA_IP_WEBCAM, AndroidIPCamEntity, CONF_HOST, CONF_NAME)
|
KEY_MAP,
|
||||||
|
DATA_IP_WEBCAM,
|
||||||
|
AndroidIPCamEntity,
|
||||||
|
CONF_HOST,
|
||||||
|
CONF_NAME,
|
||||||
|
)
|
||||||
|
|
||||||
DEPENDENCIES = ['android_ip_webcam']
|
DEPENDENCIES = ["android_ip_webcam"]
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_setup_platform(hass, config, async_add_entities,
|
def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||||
discovery_info=None):
|
|
||||||
"""Set up the IP Webcam binary sensors."""
|
"""Set up the IP Webcam binary sensors."""
|
||||||
if discovery_info is None:
|
if discovery_info is None:
|
||||||
return
|
return
|
||||||
|
|
@ -24,8 +28,7 @@ def async_setup_platform(hass, config, async_add_entities,
|
||||||
name = discovery_info[CONF_NAME]
|
name = discovery_info[CONF_NAME]
|
||||||
ipcam = hass.data[DATA_IP_WEBCAM][host]
|
ipcam = hass.data[DATA_IP_WEBCAM][host]
|
||||||
|
|
||||||
async_add_entities(
|
async_add_entities([IPWebcamBinarySensor(name, host, ipcam, "motion_active")], True)
|
||||||
[IPWebcamBinarySensor(name, host, ipcam, 'motion_active')], True)
|
|
||||||
|
|
||||||
|
|
||||||
class IPWebcamBinarySensor(AndroidIPCamEntity, BinarySensorDevice):
|
class IPWebcamBinarySensor(AndroidIPCamEntity, BinarySensorDevice):
|
||||||
|
|
@ -37,7 +40,7 @@ class IPWebcamBinarySensor(AndroidIPCamEntity, BinarySensorDevice):
|
||||||
|
|
||||||
self._sensor = sensor
|
self._sensor = sensor
|
||||||
self._mapped_name = KEY_MAP.get(self._sensor, self._sensor)
|
self._mapped_name = KEY_MAP.get(self._sensor, self._sensor)
|
||||||
self._name = '{} {}'.format(name, self._mapped_name)
|
self._name = "{} {}".format(name, self._mapped_name)
|
||||||
self._state = None
|
self._state = None
|
||||||
self._unit = None
|
self._unit = None
|
||||||
|
|
||||||
|
|
@ -60,4 +63,4 @@ class IPWebcamBinarySensor(AndroidIPCamEntity, BinarySensorDevice):
|
||||||
@property
|
@property
|
||||||
def device_class(self):
|
def device_class(self):
|
||||||
"""Return the class of this device, from component DEVICE_CLASSES."""
|
"""Return the class of this device, from component DEVICE_CLASSES."""
|
||||||
return 'motion'
|
return "motion"
|
||||||
|
|
|
||||||
|
|
@ -6,18 +6,17 @@ https://home-assistant.io/components/binary_sensor.apcupsd/
|
||||||
"""
|
"""
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import (
|
from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA
|
||||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
|
||||||
from homeassistant.const import CONF_NAME
|
from homeassistant.const import CONF_NAME
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.components import apcupsd
|
from homeassistant.components import apcupsd
|
||||||
|
|
||||||
DEFAULT_NAME = 'UPS Online Status'
|
DEFAULT_NAME = "UPS Online Status"
|
||||||
DEPENDENCIES = [apcupsd.DOMAIN]
|
DEPENDENCIES = [apcupsd.DOMAIN]
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
{vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string}
|
||||||
})
|
)
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,11 @@ import requests
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import (
|
from homeassistant.components.binary_sensor import (
|
||||||
BinarySensorDevice, PLATFORM_SCHEMA, DEVICE_CLASSES_SCHEMA)
|
BinarySensorDevice,
|
||||||
from homeassistant.const import (
|
PLATFORM_SCHEMA,
|
||||||
CONF_RESOURCE, CONF_PIN, CONF_NAME, CONF_DEVICE_CLASS)
|
DEVICE_CLASSES_SCHEMA,
|
||||||
|
)
|
||||||
|
from homeassistant.const import CONF_RESOURCE, CONF_PIN, CONF_NAME, CONF_DEVICE_CLASS
|
||||||
from homeassistant.util import Throttle
|
from homeassistant.util import Throttle
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
|
|
@ -21,12 +23,14 @@ _LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30)
|
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30)
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||||
vol.Required(CONF_RESOURCE): cv.url,
|
{
|
||||||
vol.Optional(CONF_NAME): cv.string,
|
vol.Required(CONF_RESOURCE): cv.url,
|
||||||
vol.Required(CONF_PIN): cv.string,
|
vol.Optional(CONF_NAME): cv.string,
|
||||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
vol.Required(CONF_PIN): cv.string,
|
||||||
})
|
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
|
|
@ -38,8 +42,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
try:
|
try:
|
||||||
response = requests.get(resource, timeout=10).json()
|
response = requests.get(resource, timeout=10).json()
|
||||||
except requests.exceptions.MissingSchema:
|
except requests.exceptions.MissingSchema:
|
||||||
_LOGGER.error("Missing resource or schema in configuration. "
|
_LOGGER.error(
|
||||||
"Add http:// to your URL")
|
"Missing resource or schema in configuration. " "Add http:// to your URL"
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
except requests.exceptions.ConnectionError:
|
except requests.exceptions.ConnectionError:
|
||||||
_LOGGER.error("No route to device at %s", resource)
|
_LOGGER.error("No route to device at %s", resource)
|
||||||
|
|
@ -47,9 +52,18 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
|
|
||||||
arest = ArestData(resource, pin)
|
arest = ArestData(resource, pin)
|
||||||
|
|
||||||
add_entities([ArestBinarySensor(
|
add_entities(
|
||||||
arest, resource, config.get(CONF_NAME, response[CONF_NAME]),
|
[
|
||||||
device_class, pin)], True)
|
ArestBinarySensor(
|
||||||
|
arest,
|
||||||
|
resource,
|
||||||
|
config.get(CONF_NAME, response[CONF_NAME]),
|
||||||
|
device_class,
|
||||||
|
pin,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ArestBinarySensor(BinarySensorDevice):
|
class ArestBinarySensor(BinarySensorDevice):
|
||||||
|
|
@ -65,7 +79,8 @@ class ArestBinarySensor(BinarySensorDevice):
|
||||||
|
|
||||||
if self._pin is not None:
|
if self._pin is not None:
|
||||||
request = requests.get(
|
request = requests.get(
|
||||||
'{}/mode/{}/i'.format(self._resource, self._pin), timeout=10)
|
"{}/mode/{}/i".format(self._resource, self._pin), timeout=10
|
||||||
|
)
|
||||||
if request.status_code != 200:
|
if request.status_code != 200:
|
||||||
_LOGGER.error("Can't set mode of %s", self._resource)
|
_LOGGER.error("Can't set mode of %s", self._resource)
|
||||||
|
|
||||||
|
|
@ -77,7 +92,7 @@ class ArestBinarySensor(BinarySensorDevice):
|
||||||
@property
|
@property
|
||||||
def is_on(self):
|
def is_on(self):
|
||||||
"""Return true if the binary sensor is on."""
|
"""Return true if the binary sensor is on."""
|
||||||
return bool(self.arest.data.get('state'))
|
return bool(self.arest.data.get("state"))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device_class(self):
|
def device_class(self):
|
||||||
|
|
@ -102,8 +117,9 @@ class ArestData:
|
||||||
def update(self):
|
def update(self):
|
||||||
"""Get the latest data from aREST device."""
|
"""Get the latest data from aREST device."""
|
||||||
try:
|
try:
|
||||||
response = requests.get('{}/digital/{}'.format(
|
response = requests.get(
|
||||||
self._resource, self._pin), timeout=10)
|
"{}/digital/{}".format(self._resource, self._pin), timeout=10
|
||||||
self.data = {'state': response.json()['return_value']}
|
)
|
||||||
|
self.data = {"state": response.json()["return_value"]}
|
||||||
except requests.exceptions.ConnectionError:
|
except requests.exceptions.ConnectionError:
|
||||||
_LOGGER.error("No route to device '%s'", self._resource)
|
_LOGGER.error("No route to device '%s'", self._resource)
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,9 @@ https://home-assistant.io/components/sensor.august/
|
||||||
from datetime import timedelta, datetime
|
from datetime import timedelta, datetime
|
||||||
|
|
||||||
from homeassistant.components.august import DATA_AUGUST
|
from homeassistant.components.august import DATA_AUGUST
|
||||||
from homeassistant.components.binary_sensor import (BinarySensorDevice)
|
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||||
|
|
||||||
DEPENDENCIES = ['august']
|
DEPENDENCIES = ["august"]
|
||||||
|
|
||||||
SCAN_INTERVAL = timedelta(seconds=5)
|
SCAN_INTERVAL = timedelta(seconds=5)
|
||||||
|
|
||||||
|
|
@ -22,21 +22,21 @@ def _retrieve_online_state(data, doorbell):
|
||||||
|
|
||||||
def _retrieve_motion_state(data, doorbell):
|
def _retrieve_motion_state(data, doorbell):
|
||||||
from august.activity import ActivityType
|
from august.activity import ActivityType
|
||||||
return _activity_time_based_state(data, doorbell,
|
|
||||||
[ActivityType.DOORBELL_MOTION,
|
return _activity_time_based_state(
|
||||||
ActivityType.DOORBELL_DING])
|
data, doorbell, [ActivityType.DOORBELL_MOTION, ActivityType.DOORBELL_DING]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _retrieve_ding_state(data, doorbell):
|
def _retrieve_ding_state(data, doorbell):
|
||||||
from august.activity import ActivityType
|
from august.activity import ActivityType
|
||||||
return _activity_time_based_state(data, doorbell,
|
|
||||||
[ActivityType.DOORBELL_DING])
|
return _activity_time_based_state(data, doorbell, [ActivityType.DOORBELL_DING])
|
||||||
|
|
||||||
|
|
||||||
def _activity_time_based_state(data, doorbell, activity_types):
|
def _activity_time_based_state(data, doorbell, activity_types):
|
||||||
"""Get the latest state of the sensor."""
|
"""Get the latest state of the sensor."""
|
||||||
latest = data.get_latest_device_activity(doorbell.device_id,
|
latest = data.get_latest_device_activity(doorbell.device_id, *activity_types)
|
||||||
*activity_types)
|
|
||||||
|
|
||||||
if latest is not None:
|
if latest is not None:
|
||||||
start = latest.activity_start_time
|
start = latest.activity_start_time
|
||||||
|
|
@ -47,9 +47,9 @@ def _activity_time_based_state(data, doorbell, activity_types):
|
||||||
|
|
||||||
# Sensor types: Name, device_class, state_provider
|
# Sensor types: Name, device_class, state_provider
|
||||||
SENSOR_TYPES = {
|
SENSOR_TYPES = {
|
||||||
'doorbell_ding': ['Ding', 'occupancy', _retrieve_ding_state],
|
"doorbell_ding": ["Ding", "occupancy", _retrieve_ding_state],
|
||||||
'doorbell_motion': ['Motion', 'motion', _retrieve_motion_state],
|
"doorbell_motion": ["Motion", "motion", _retrieve_motion_state],
|
||||||
'doorbell_online': ['Online', 'connectivity', _retrieve_online_state],
|
"doorbell_online": ["Online", "connectivity", _retrieve_online_state],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -88,8 +88,9 @@ class AugustBinarySensor(BinarySensorDevice):
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Return the name of the binary sensor."""
|
"""Return the name of the binary sensor."""
|
||||||
return "{} {}".format(self._doorbell.device_name,
|
return "{} {}".format(
|
||||||
SENSOR_TYPES[self._sensor_type][0])
|
self._doorbell.device_name, SENSOR_TYPES[self._sensor_type][0]
|
||||||
|
)
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
"""Get the latest state of the sensor."""
|
"""Get the latest state of the sensor."""
|
||||||
|
|
|
||||||
|
|
@ -11,20 +11,18 @@ from aiohttp.hdrs import USER_AGENT
|
||||||
import requests
|
import requests
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import (
|
from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice
|
||||||
PLATFORM_SCHEMA, BinarySensorDevice)
|
|
||||||
from homeassistant.const import CONF_NAME, ATTR_ATTRIBUTION
|
from homeassistant.const import CONF_NAME, ATTR_ATTRIBUTION
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.util import Throttle
|
from homeassistant.util import Throttle
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
CONF_ATTRIBUTION = "Data provided by the National Oceanic and Atmospheric" \
|
CONF_ATTRIBUTION = "Data provided by the National Oceanic and Atmospheric" "Administration"
|
||||||
"Administration"
|
CONF_THRESHOLD = "forecast_threshold"
|
||||||
CONF_THRESHOLD = 'forecast_threshold'
|
|
||||||
|
|
||||||
DEFAULT_DEVICE_CLASS = 'visible'
|
DEFAULT_DEVICE_CLASS = "visible"
|
||||||
DEFAULT_NAME = 'Aurora Visibility'
|
DEFAULT_NAME = "Aurora Visibility"
|
||||||
DEFAULT_THRESHOLD = 75
|
DEFAULT_THRESHOLD = 75
|
||||||
|
|
||||||
HA_USER_AGENT = "Home Assistant Aurora Tracker v.0.1.0"
|
HA_USER_AGENT = "Home Assistant Aurora Tracker v.0.1.0"
|
||||||
|
|
@ -33,10 +31,12 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5)
|
||||||
|
|
||||||
URL = "http://services.swpc.noaa.gov/text/aurora-nowcast-map.txt"
|
URL = "http://services.swpc.noaa.gov/text/aurora-nowcast-map.txt"
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
{
|
||||||
vol.Optional(CONF_THRESHOLD, default=DEFAULT_THRESHOLD): cv.positive_int,
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||||
})
|
vol.Optional(CONF_THRESHOLD, default=DEFAULT_THRESHOLD): cv.positive_int,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
|
|
@ -49,12 +49,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
threshold = config.get(CONF_THRESHOLD)
|
threshold = config.get(CONF_THRESHOLD)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
aurora_data = AuroraData(
|
aurora_data = AuroraData(hass.config.latitude, hass.config.longitude, threshold)
|
||||||
hass.config.latitude, hass.config.longitude, threshold)
|
|
||||||
aurora_data.update()
|
aurora_data.update()
|
||||||
except requests.exceptions.HTTPError as error:
|
except requests.exceptions.HTTPError as error:
|
||||||
_LOGGER.error(
|
_LOGGER.error("Connection to aurora forecast service failed: %s", error)
|
||||||
"Connection to aurora forecast service failed: %s", error)
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
add_entities([AuroraSensor(aurora_data, name)], True)
|
add_entities([AuroraSensor(aurora_data, name)], True)
|
||||||
|
|
@ -71,7 +69,7 @@ class AuroraSensor(BinarySensorDevice):
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Return the name of the sensor."""
|
"""Return the name of the sensor."""
|
||||||
return '{}'.format(self._name)
|
return "{}".format(self._name)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self):
|
def is_on(self):
|
||||||
|
|
@ -89,8 +87,8 @@ class AuroraSensor(BinarySensorDevice):
|
||||||
attrs = {}
|
attrs = {}
|
||||||
|
|
||||||
if self.aurora_data:
|
if self.aurora_data:
|
||||||
attrs['visibility_level'] = self.aurora_data.visibility_level
|
attrs["visibility_level"] = self.aurora_data.visibility_level
|
||||||
attrs['message'] = self.aurora_data.is_visible_text
|
attrs["message"] = self.aurora_data.is_visible_text
|
||||||
attrs[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION
|
attrs[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
|
@ -127,8 +125,7 @@ class AuroraData:
|
||||||
self.is_visible_text = "nothing's out"
|
self.is_visible_text = "nothing's out"
|
||||||
|
|
||||||
except requests.exceptions.HTTPError as error:
|
except requests.exceptions.HTTPError as error:
|
||||||
_LOGGER.error(
|
_LOGGER.error("Connection to aurora forecast service failed: %s", error)
|
||||||
"Connection to aurora forecast service failed: %s", error)
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_aurora_forecast(self):
|
def get_aurora_forecast(self):
|
||||||
|
|
@ -141,9 +138,11 @@ class AuroraData:
|
||||||
]
|
]
|
||||||
|
|
||||||
# Convert lat and long for data points in table
|
# Convert lat and long for data points in table
|
||||||
converted_latitude = round((self.latitude / 180)
|
converted_latitude = round(
|
||||||
* self.number_of_latitude_intervals)
|
(self.latitude / 180) * self.number_of_latitude_intervals
|
||||||
converted_longitude = round((self.longitude / 360)
|
)
|
||||||
* self.number_of_longitude_intervals)
|
converted_longitude = round(
|
||||||
|
(self.longitude / 360) * self.number_of_longitude_intervals
|
||||||
|
)
|
||||||
|
|
||||||
return forecast_table[converted_latitude][converted_longitude]
|
return forecast_table[converted_latitude][converted_longitude]
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ from homeassistant.const import CONF_TRIGGER_TIME
|
||||||
from homeassistant.helpers.event import track_point_in_utc_time
|
from homeassistant.helpers.event import track_point_in_utc_time
|
||||||
from homeassistant.util.dt import utcnow
|
from homeassistant.util.dt import utcnow
|
||||||
|
|
||||||
DEPENDENCIES = ['axis']
|
DEPENDENCIES = ["axis"]
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -55,13 +55,14 @@ class AxisBinarySensor(AxisDeviceEvent, BinarySensorDevice):
|
||||||
# Set timer to wait until updating the state
|
# Set timer to wait until updating the state
|
||||||
def _delay_update(now):
|
def _delay_update(now):
|
||||||
"""Timer callback for sensor update."""
|
"""Timer callback for sensor update."""
|
||||||
_LOGGER.debug("%s called delayed (%s sec) update",
|
_LOGGER.debug(
|
||||||
self._name, self._delay)
|
"%s called delayed (%s sec) update", self._name, self._delay
|
||||||
|
)
|
||||||
self.schedule_update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
self._timer = None
|
self._timer = None
|
||||||
|
|
||||||
self._timer = track_point_in_utc_time(
|
self._timer = track_point_in_utc_time(
|
||||||
self.hass, _delay_update,
|
self.hass, _delay_update, utcnow() + timedelta(seconds=self._delay)
|
||||||
utcnow() + timedelta(seconds=self._delay))
|
)
|
||||||
else:
|
else:
|
||||||
self.schedule_update_ha_state()
|
self.schedule_update_ha_state()
|
||||||
|
|
|
||||||
|
|
@ -11,58 +11,73 @@ from collections import OrderedDict
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.components.binary_sensor import (
|
from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA
|
||||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_ABOVE, CONF_BELOW, CONF_DEVICE_CLASS, CONF_ENTITY_ID, CONF_NAME,
|
CONF_ABOVE,
|
||||||
CONF_PLATFORM, CONF_STATE, STATE_UNKNOWN)
|
CONF_BELOW,
|
||||||
|
CONF_DEVICE_CLASS,
|
||||||
|
CONF_ENTITY_ID,
|
||||||
|
CONF_NAME,
|
||||||
|
CONF_PLATFORM,
|
||||||
|
CONF_STATE,
|
||||||
|
STATE_UNKNOWN,
|
||||||
|
)
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.helpers import condition
|
from homeassistant.helpers import condition
|
||||||
from homeassistant.helpers.event import async_track_state_change
|
from homeassistant.helpers.event import async_track_state_change
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
ATTR_OBSERVATIONS = 'observations'
|
ATTR_OBSERVATIONS = "observations"
|
||||||
ATTR_PROBABILITY = 'probability'
|
ATTR_PROBABILITY = "probability"
|
||||||
ATTR_PROBABILITY_THRESHOLD = 'probability_threshold'
|
ATTR_PROBABILITY_THRESHOLD = "probability_threshold"
|
||||||
|
|
||||||
CONF_OBSERVATIONS = 'observations'
|
CONF_OBSERVATIONS = "observations"
|
||||||
CONF_PRIOR = 'prior'
|
CONF_PRIOR = "prior"
|
||||||
CONF_PROBABILITY_THRESHOLD = 'probability_threshold'
|
CONF_PROBABILITY_THRESHOLD = "probability_threshold"
|
||||||
CONF_P_GIVEN_F = 'prob_given_false'
|
CONF_P_GIVEN_F = "prob_given_false"
|
||||||
CONF_P_GIVEN_T = 'prob_given_true'
|
CONF_P_GIVEN_T = "prob_given_true"
|
||||||
CONF_TO_STATE = 'to_state'
|
CONF_TO_STATE = "to_state"
|
||||||
|
|
||||||
DEFAULT_NAME = "Bayesian Binary Sensor"
|
DEFAULT_NAME = "Bayesian Binary Sensor"
|
||||||
DEFAULT_PROBABILITY_THRESHOLD = 0.5
|
DEFAULT_PROBABILITY_THRESHOLD = 0.5
|
||||||
|
|
||||||
NUMERIC_STATE_SCHEMA = vol.Schema({
|
NUMERIC_STATE_SCHEMA = vol.Schema(
|
||||||
CONF_PLATFORM: 'numeric_state',
|
{
|
||||||
vol.Required(CONF_ENTITY_ID): cv.entity_id,
|
CONF_PLATFORM: "numeric_state",
|
||||||
vol.Optional(CONF_ABOVE): vol.Coerce(float),
|
vol.Required(CONF_ENTITY_ID): cv.entity_id,
|
||||||
vol.Optional(CONF_BELOW): vol.Coerce(float),
|
vol.Optional(CONF_ABOVE): vol.Coerce(float),
|
||||||
vol.Required(CONF_P_GIVEN_T): vol.Coerce(float),
|
vol.Optional(CONF_BELOW): vol.Coerce(float),
|
||||||
vol.Optional(CONF_P_GIVEN_F): vol.Coerce(float)
|
vol.Required(CONF_P_GIVEN_T): vol.Coerce(float),
|
||||||
}, required=True)
|
vol.Optional(CONF_P_GIVEN_F): vol.Coerce(float),
|
||||||
|
},
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
|
||||||
STATE_SCHEMA = vol.Schema({
|
STATE_SCHEMA = vol.Schema(
|
||||||
CONF_PLATFORM: CONF_STATE,
|
{
|
||||||
vol.Required(CONF_ENTITY_ID): cv.entity_id,
|
CONF_PLATFORM: CONF_STATE,
|
||||||
vol.Required(CONF_TO_STATE): cv.string,
|
vol.Required(CONF_ENTITY_ID): cv.entity_id,
|
||||||
vol.Required(CONF_P_GIVEN_T): vol.Coerce(float),
|
vol.Required(CONF_TO_STATE): cv.string,
|
||||||
vol.Optional(CONF_P_GIVEN_F): vol.Coerce(float)
|
vol.Required(CONF_P_GIVEN_T): vol.Coerce(float),
|
||||||
}, required=True)
|
vol.Optional(CONF_P_GIVEN_F): vol.Coerce(float),
|
||||||
|
},
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
{
|
||||||
vol.Optional(CONF_DEVICE_CLASS): cv.string,
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||||
vol.Required(CONF_OBSERVATIONS):
|
vol.Optional(CONF_DEVICE_CLASS): cv.string,
|
||||||
vol.Schema(vol.All(cv.ensure_list,
|
vol.Required(CONF_OBSERVATIONS): vol.Schema(
|
||||||
[vol.Any(NUMERIC_STATE_SCHEMA, STATE_SCHEMA)])),
|
vol.All(cv.ensure_list, [vol.Any(NUMERIC_STATE_SCHEMA, STATE_SCHEMA)])
|
||||||
vol.Required(CONF_PRIOR): vol.Coerce(float),
|
),
|
||||||
vol.Optional(CONF_PROBABILITY_THRESHOLD,
|
vol.Required(CONF_PRIOR): vol.Coerce(float),
|
||||||
default=DEFAULT_PROBABILITY_THRESHOLD): vol.Coerce(float),
|
vol.Optional(
|
||||||
})
|
CONF_PROBABILITY_THRESHOLD, default=DEFAULT_PROBABILITY_THRESHOLD
|
||||||
|
): vol.Coerce(float),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def update_probability(prior, prob_true, prob_false):
|
def update_probability(prior, prob_true, prob_false):
|
||||||
|
|
@ -75,8 +90,7 @@ def update_probability(prior, prob_true, prob_false):
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_setup_platform(hass, config, async_add_entities,
|
def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||||
discovery_info=None):
|
|
||||||
"""Set up the Bayesian Binary sensor."""
|
"""Set up the Bayesian Binary sensor."""
|
||||||
name = config.get(CONF_NAME)
|
name = config.get(CONF_NAME)
|
||||||
observations = config.get(CONF_OBSERVATIONS)
|
observations = config.get(CONF_OBSERVATIONS)
|
||||||
|
|
@ -84,17 +98,20 @@ def async_setup_platform(hass, config, async_add_entities,
|
||||||
probability_threshold = config.get(CONF_PROBABILITY_THRESHOLD)
|
probability_threshold = config.get(CONF_PROBABILITY_THRESHOLD)
|
||||||
device_class = config.get(CONF_DEVICE_CLASS)
|
device_class = config.get(CONF_DEVICE_CLASS)
|
||||||
|
|
||||||
async_add_entities([
|
async_add_entities(
|
||||||
BayesianBinarySensor(
|
[
|
||||||
name, prior, observations, probability_threshold, device_class)
|
BayesianBinarySensor(
|
||||||
], True)
|
name, prior, observations, probability_threshold, device_class
|
||||||
|
)
|
||||||
|
],
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class BayesianBinarySensor(BinarySensorDevice):
|
class BayesianBinarySensor(BinarySensorDevice):
|
||||||
"""Representation of a Bayesian sensor."""
|
"""Representation of a Bayesian sensor."""
|
||||||
|
|
||||||
def __init__(self, name, prior, observations, probability_threshold,
|
def __init__(self, name, prior, observations, probability_threshold, device_class):
|
||||||
device_class):
|
|
||||||
"""Initialize the Bayesian sensor."""
|
"""Initialize the Bayesian sensor."""
|
||||||
self._name = name
|
self._name = name
|
||||||
self._observations = observations
|
self._observations = observations
|
||||||
|
|
@ -106,25 +123,25 @@ class BayesianBinarySensor(BinarySensorDevice):
|
||||||
|
|
||||||
self.current_obs = OrderedDict({})
|
self.current_obs = OrderedDict({})
|
||||||
|
|
||||||
to_observe = set(obs['entity_id'] for obs in self._observations)
|
to_observe = set(obs["entity_id"] for obs in self._observations)
|
||||||
|
|
||||||
self.entity_obs = dict.fromkeys(to_observe, [])
|
self.entity_obs = dict.fromkeys(to_observe, [])
|
||||||
|
|
||||||
for ind, obs in enumerate(self._observations):
|
for ind, obs in enumerate(self._observations):
|
||||||
obs['id'] = ind
|
obs["id"] = ind
|
||||||
self.entity_obs[obs['entity_id']].append(obs)
|
self.entity_obs[obs["entity_id"]].append(obs)
|
||||||
|
|
||||||
self.watchers = {
|
self.watchers = {
|
||||||
'numeric_state': self._process_numeric_state,
|
"numeric_state": self._process_numeric_state,
|
||||||
'state': self._process_state
|
"state": self._process_state,
|
||||||
}
|
}
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_added_to_hass(self):
|
def async_added_to_hass(self):
|
||||||
"""Call when entity about to be added."""
|
"""Call when entity about to be added."""
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_threshold_sensor_state_listener(entity, old_state,
|
def async_threshold_sensor_state_listener(entity, old_state, new_state):
|
||||||
new_state):
|
|
||||||
"""Handle sensor state changes."""
|
"""Handle sensor state changes."""
|
||||||
if new_state.state == STATE_UNKNOWN:
|
if new_state.state == STATE_UNKNOWN:
|
||||||
return
|
return
|
||||||
|
|
@ -132,34 +149,33 @@ class BayesianBinarySensor(BinarySensorDevice):
|
||||||
entity_obs_list = self.entity_obs[entity]
|
entity_obs_list = self.entity_obs[entity]
|
||||||
|
|
||||||
for entity_obs in entity_obs_list:
|
for entity_obs in entity_obs_list:
|
||||||
platform = entity_obs['platform']
|
platform = entity_obs["platform"]
|
||||||
|
|
||||||
self.watchers[platform](entity_obs)
|
self.watchers[platform](entity_obs)
|
||||||
|
|
||||||
prior = self.prior
|
prior = self.prior
|
||||||
for obs in self.current_obs.values():
|
for obs in self.current_obs.values():
|
||||||
prior = update_probability(
|
prior = update_probability(prior, obs["prob_true"], obs["prob_false"])
|
||||||
prior, obs['prob_true'], obs['prob_false'])
|
|
||||||
self.probability = prior
|
self.probability = prior
|
||||||
|
|
||||||
self.hass.async_add_job(self.async_update_ha_state, True)
|
self.hass.async_add_job(self.async_update_ha_state, True)
|
||||||
|
|
||||||
entities = [obs['entity_id'] for obs in self._observations]
|
entities = [obs["entity_id"] for obs in self._observations]
|
||||||
async_track_state_change(
|
async_track_state_change(
|
||||||
self.hass, entities, async_threshold_sensor_state_listener)
|
self.hass, entities, async_threshold_sensor_state_listener
|
||||||
|
)
|
||||||
|
|
||||||
def _update_current_obs(self, entity_observation, should_trigger):
|
def _update_current_obs(self, entity_observation, should_trigger):
|
||||||
"""Update current observation."""
|
"""Update current observation."""
|
||||||
obs_id = entity_observation['id']
|
obs_id = entity_observation["id"]
|
||||||
|
|
||||||
if should_trigger:
|
if should_trigger:
|
||||||
prob_true = entity_observation['prob_given_true']
|
prob_true = entity_observation["prob_given_true"]
|
||||||
prob_false = entity_observation.get(
|
prob_false = entity_observation.get("prob_given_false", 1 - prob_true)
|
||||||
'prob_given_false', 1 - prob_true)
|
|
||||||
|
|
||||||
self.current_obs[obs_id] = {
|
self.current_obs[obs_id] = {
|
||||||
'prob_true': prob_true,
|
"prob_true": prob_true,
|
||||||
'prob_false': prob_false
|
"prob_false": prob_false,
|
||||||
}
|
}
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|
@ -167,21 +183,26 @@ class BayesianBinarySensor(BinarySensorDevice):
|
||||||
|
|
||||||
def _process_numeric_state(self, entity_observation):
|
def _process_numeric_state(self, entity_observation):
|
||||||
"""Add entity to current_obs if numeric state conditions are met."""
|
"""Add entity to current_obs if numeric state conditions are met."""
|
||||||
entity = entity_observation['entity_id']
|
entity = entity_observation["entity_id"]
|
||||||
|
|
||||||
should_trigger = condition.async_numeric_state(
|
should_trigger = condition.async_numeric_state(
|
||||||
self.hass, entity,
|
self.hass,
|
||||||
entity_observation.get('below'),
|
entity,
|
||||||
entity_observation.get('above'), None, entity_observation)
|
entity_observation.get("below"),
|
||||||
|
entity_observation.get("above"),
|
||||||
|
None,
|
||||||
|
entity_observation,
|
||||||
|
)
|
||||||
|
|
||||||
self._update_current_obs(entity_observation, should_trigger)
|
self._update_current_obs(entity_observation, should_trigger)
|
||||||
|
|
||||||
def _process_state(self, entity_observation):
|
def _process_state(self, entity_observation):
|
||||||
"""Add entity to current observations if state conditions are met."""
|
"""Add entity to current observations if state conditions are met."""
|
||||||
entity = entity_observation['entity_id']
|
entity = entity_observation["entity_id"]
|
||||||
|
|
||||||
should_trigger = condition.state(
|
should_trigger = condition.state(
|
||||||
self.hass, entity, entity_observation.get('to_state'))
|
self.hass, entity, entity_observation.get("to_state")
|
||||||
|
)
|
||||||
|
|
||||||
self._update_current_obs(entity_observation, should_trigger)
|
self._update_current_obs(entity_observation, should_trigger)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,36 +9,35 @@ import logging
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components import bbb_gpio
|
from homeassistant.components import bbb_gpio
|
||||||
from homeassistant.components.binary_sensor import (
|
from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA
|
||||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
from homeassistant.const import DEVICE_DEFAULT_NAME, CONF_NAME
|
||||||
from homeassistant.const import (DEVICE_DEFAULT_NAME, CONF_NAME)
|
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DEPENDENCIES = ['bbb_gpio']
|
DEPENDENCIES = ["bbb_gpio"]
|
||||||
|
|
||||||
CONF_PINS = 'pins'
|
CONF_PINS = "pins"
|
||||||
CONF_BOUNCETIME = 'bouncetime'
|
CONF_BOUNCETIME = "bouncetime"
|
||||||
CONF_INVERT_LOGIC = 'invert_logic'
|
CONF_INVERT_LOGIC = "invert_logic"
|
||||||
CONF_PULL_MODE = 'pull_mode'
|
CONF_PULL_MODE = "pull_mode"
|
||||||
|
|
||||||
DEFAULT_BOUNCETIME = 50
|
DEFAULT_BOUNCETIME = 50
|
||||||
DEFAULT_INVERT_LOGIC = False
|
DEFAULT_INVERT_LOGIC = False
|
||||||
DEFAULT_PULL_MODE = 'UP'
|
DEFAULT_PULL_MODE = "UP"
|
||||||
|
|
||||||
PIN_SCHEMA = vol.Schema({
|
PIN_SCHEMA = vol.Schema(
|
||||||
vol.Required(CONF_NAME): cv.string,
|
{
|
||||||
vol.Optional(CONF_BOUNCETIME, default=DEFAULT_BOUNCETIME): cv.positive_int,
|
vol.Required(CONF_NAME): cv.string,
|
||||||
vol.Optional(CONF_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean,
|
vol.Optional(CONF_BOUNCETIME, default=DEFAULT_BOUNCETIME): cv.positive_int,
|
||||||
vol.Optional(CONF_PULL_MODE, default=DEFAULT_PULL_MODE):
|
vol.Optional(CONF_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean,
|
||||||
vol.In(['UP', 'DOWN'])
|
vol.Optional(CONF_PULL_MODE, default=DEFAULT_PULL_MODE): vol.In(["UP", "DOWN"]),
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||||
vol.Required(CONF_PINS, default={}):
|
{vol.Required(CONF_PINS, default={}): vol.Schema({cv.string: PIN_SCHEMA})}
|
||||||
vol.Schema({cv.string: PIN_SCHEMA}),
|
)
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ https://home-assistant.io/components/binary_sensor.blink/
|
||||||
from homeassistant.components.blink import DOMAIN
|
from homeassistant.components.blink import DOMAIN
|
||||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||||
|
|
||||||
DEPENDENCIES = ['blink']
|
DEPENDENCIES = ["blink"]
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
|
|
@ -28,7 +28,7 @@ class BlinkCameraMotionSensor(BinarySensorDevice):
|
||||||
|
|
||||||
def __init__(self, name, data):
|
def __init__(self, name, data):
|
||||||
"""Initialize the sensor."""
|
"""Initialize the sensor."""
|
||||||
self._name = 'blink_' + name + '_motion_enabled'
|
self._name = "blink_" + name + "_motion_enabled"
|
||||||
self._camera_name = name
|
self._camera_name = name
|
||||||
self.data = data
|
self.data = data
|
||||||
self._state = self.data.cameras[self._camera_name].armed
|
self._state = self.data.cameras[self._camera_name].armed
|
||||||
|
|
@ -54,7 +54,7 @@ class BlinkSystemSensor(BinarySensorDevice):
|
||||||
|
|
||||||
def __init__(self, data):
|
def __init__(self, data):
|
||||||
"""Initialize the sensor."""
|
"""Initialize the sensor."""
|
||||||
self._name = 'blink armed status'
|
self._name = "blink armed status"
|
||||||
self.data = data
|
self.data = data
|
||||||
self._state = self.data.arm
|
self._state = self.data.arm
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,24 +8,23 @@ import logging
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import (
|
from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA
|
||||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
|
||||||
from homeassistant.const import CONF_MONITORED_CONDITIONS
|
from homeassistant.const import CONF_MONITORED_CONDITIONS
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DEPENDENCIES = ['bloomsky']
|
DEPENDENCIES = ["bloomsky"]
|
||||||
|
|
||||||
SENSOR_TYPES = {
|
SENSOR_TYPES = {"Rain": "moisture", "Night": None}
|
||||||
'Rain': 'moisture',
|
|
||||||
'Night': None,
|
|
||||||
}
|
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||||
vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)):
|
{
|
||||||
vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
|
vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)): vol.All(
|
||||||
})
|
cv.ensure_list, [vol.In(SENSOR_TYPES)]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
|
|
@ -36,8 +35,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
|
|
||||||
for device in bloomsky.BLOOMSKY.devices.values():
|
for device in bloomsky.BLOOMSKY.devices.values():
|
||||||
for variable in sensors:
|
for variable in sensors:
|
||||||
add_entities(
|
add_entities([BloomSkySensor(bloomsky.BLOOMSKY, device, variable)], True)
|
||||||
[BloomSkySensor(bloomsky.BLOOMSKY, device, variable)], True)
|
|
||||||
|
|
||||||
|
|
||||||
class BloomSkySensor(BinarySensorDevice):
|
class BloomSkySensor(BinarySensorDevice):
|
||||||
|
|
@ -46,9 +44,9 @@ class BloomSkySensor(BinarySensorDevice):
|
||||||
def __init__(self, bs, device, sensor_name):
|
def __init__(self, bs, device, sensor_name):
|
||||||
"""Initialize a BloomSky binary sensor."""
|
"""Initialize a BloomSky binary sensor."""
|
||||||
self._bloomsky = bs
|
self._bloomsky = bs
|
||||||
self._device_id = device['DeviceID']
|
self._device_id = device["DeviceID"]
|
||||||
self._sensor_name = sensor_name
|
self._sensor_name = sensor_name
|
||||||
self._name = '{} {}'.format(device['DeviceName'], sensor_name)
|
self._name = "{} {}".format(device["DeviceName"], sensor_name)
|
||||||
self._state = None
|
self._state = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
@ -70,5 +68,4 @@ class BloomSkySensor(BinarySensorDevice):
|
||||||
"""Request an update from the BloomSky API."""
|
"""Request an update from the BloomSky API."""
|
||||||
self._bloomsky.refresh_devices()
|
self._bloomsky.refresh_devices()
|
||||||
|
|
||||||
self._state = \
|
self._state = self._bloomsky.devices[self._device_id]["Data"][self._sensor_name]
|
||||||
self._bloomsky.devices[self._device_id]['Data'][self._sensor_name]
|
|
||||||
|
|
|
||||||
|
|
@ -10,22 +10,22 @@ import logging
|
||||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||||
from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN
|
from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN
|
||||||
|
|
||||||
DEPENDENCIES = ['bmw_connected_drive']
|
DEPENDENCIES = ["bmw_connected_drive"]
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
SENSOR_TYPES = {
|
SENSOR_TYPES = {
|
||||||
'lids': ['Doors', 'opening'],
|
"lids": ["Doors", "opening"],
|
||||||
'windows': ['Windows', 'opening'],
|
"windows": ["Windows", "opening"],
|
||||||
'door_lock_state': ['Door lock state', 'safety'],
|
"door_lock_state": ["Door lock state", "safety"],
|
||||||
'lights_parking': ['Parking lights', 'light'],
|
"lights_parking": ["Parking lights", "light"],
|
||||||
'condition_based_services': ['Condition based services', 'problem'],
|
"condition_based_services": ["Condition based services", "problem"],
|
||||||
'check_control_messages': ['Control messages', 'problem']
|
"check_control_messages": ["Control messages", "problem"],
|
||||||
}
|
}
|
||||||
|
|
||||||
SENSOR_TYPES_ELEC = {
|
SENSOR_TYPES_ELEC = {
|
||||||
'charging_status': ['Charging status', 'power'],
|
"charging_status": ["Charging status", "power"],
|
||||||
'connection_status': ['Connection status', 'plug']
|
"connection_status": ["Connection status", "plug"],
|
||||||
}
|
}
|
||||||
|
|
||||||
SENSOR_TYPES_ELEC.update(SENSOR_TYPES)
|
SENSOR_TYPES_ELEC.update(SENSOR_TYPES)
|
||||||
|
|
@ -34,22 +34,23 @@ SENSOR_TYPES_ELEC.update(SENSOR_TYPES)
|
||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
"""Set up the BMW sensors."""
|
"""Set up the BMW sensors."""
|
||||||
accounts = hass.data[BMW_DOMAIN]
|
accounts = hass.data[BMW_DOMAIN]
|
||||||
_LOGGER.debug('Found BMW accounts: %s',
|
_LOGGER.debug("Found BMW accounts: %s", ", ".join([a.name for a in accounts]))
|
||||||
', '.join([a.name for a in accounts]))
|
|
||||||
devices = []
|
devices = []
|
||||||
for account in accounts:
|
for account in accounts:
|
||||||
for vehicle in account.account.vehicles:
|
for vehicle in account.account.vehicles:
|
||||||
if vehicle.has_hv_battery:
|
if vehicle.has_hv_battery:
|
||||||
_LOGGER.debug('BMW with a high voltage battery')
|
_LOGGER.debug("BMW with a high voltage battery")
|
||||||
for key, value in sorted(SENSOR_TYPES_ELEC.items()):
|
for key, value in sorted(SENSOR_TYPES_ELEC.items()):
|
||||||
device = BMWConnectedDriveSensor(account, vehicle, key,
|
device = BMWConnectedDriveSensor(
|
||||||
value[0], value[1])
|
account, vehicle, key, value[0], value[1]
|
||||||
|
)
|
||||||
devices.append(device)
|
devices.append(device)
|
||||||
elif vehicle.has_internal_combustion_engine:
|
elif vehicle.has_internal_combustion_engine:
|
||||||
_LOGGER.debug('BMW with an internal combustion engine')
|
_LOGGER.debug("BMW with an internal combustion engine")
|
||||||
for key, value in sorted(SENSOR_TYPES.items()):
|
for key, value in sorted(SENSOR_TYPES.items()):
|
||||||
device = BMWConnectedDriveSensor(account, vehicle, key,
|
device = BMWConnectedDriveSensor(
|
||||||
value[0], value[1])
|
account, vehicle, key, value[0], value[1]
|
||||||
|
)
|
||||||
devices.append(device)
|
devices.append(device)
|
||||||
add_entities(devices, True)
|
add_entities(devices, True)
|
||||||
|
|
||||||
|
|
@ -57,14 +58,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
class BMWConnectedDriveSensor(BinarySensorDevice):
|
class BMWConnectedDriveSensor(BinarySensorDevice):
|
||||||
"""Representation of a BMW vehicle binary sensor."""
|
"""Representation of a BMW vehicle binary sensor."""
|
||||||
|
|
||||||
def __init__(self, account, vehicle, attribute: str, sensor_name,
|
def __init__(self, account, vehicle, attribute: str, sensor_name, device_class):
|
||||||
device_class):
|
|
||||||
"""Constructor."""
|
"""Constructor."""
|
||||||
self._account = account
|
self._account = account
|
||||||
self._vehicle = vehicle
|
self._vehicle = vehicle
|
||||||
self._attribute = attribute
|
self._attribute = attribute
|
||||||
self._name = '{} {}'.format(self._vehicle.name, self._attribute)
|
self._name = "{} {}".format(self._vehicle.name, self._attribute)
|
||||||
self._unique_id = '{}-{}'.format(self._vehicle.vin, self._attribute)
|
self._unique_id = "{}-{}".format(self._vehicle.vin, self._attribute)
|
||||||
self._sensor_name = sensor_name
|
self._sensor_name = sensor_name
|
||||||
self._device_class = device_class
|
self._device_class = device_class
|
||||||
self._state = None
|
self._state = None
|
||||||
|
|
@ -101,39 +101,37 @@ class BMWConnectedDriveSensor(BinarySensorDevice):
|
||||||
def device_state_attributes(self):
|
def device_state_attributes(self):
|
||||||
"""Return the state attributes of the binary sensor."""
|
"""Return the state attributes of the binary sensor."""
|
||||||
vehicle_state = self._vehicle.state
|
vehicle_state = self._vehicle.state
|
||||||
result = {
|
result = {"car": self._vehicle.name}
|
||||||
'car': self._vehicle.name
|
|
||||||
}
|
|
||||||
|
|
||||||
if self._attribute == 'lids':
|
if self._attribute == "lids":
|
||||||
for lid in vehicle_state.lids:
|
for lid in vehicle_state.lids:
|
||||||
result[lid.name] = lid.state.value
|
result[lid.name] = lid.state.value
|
||||||
elif self._attribute == 'windows':
|
elif self._attribute == "windows":
|
||||||
for window in vehicle_state.windows:
|
for window in vehicle_state.windows:
|
||||||
result[window.name] = window.state.value
|
result[window.name] = window.state.value
|
||||||
elif self._attribute == 'door_lock_state':
|
elif self._attribute == "door_lock_state":
|
||||||
result['door_lock_state'] = vehicle_state.door_lock_state.value
|
result["door_lock_state"] = vehicle_state.door_lock_state.value
|
||||||
result['last_update_reason'] = vehicle_state.last_update_reason
|
result["last_update_reason"] = vehicle_state.last_update_reason
|
||||||
elif self._attribute == 'lights_parking':
|
elif self._attribute == "lights_parking":
|
||||||
result['lights_parking'] = vehicle_state.parking_lights.value
|
result["lights_parking"] = vehicle_state.parking_lights.value
|
||||||
elif self._attribute == 'condition_based_services':
|
elif self._attribute == "condition_based_services":
|
||||||
for report in vehicle_state.condition_based_services:
|
for report in vehicle_state.condition_based_services:
|
||||||
result.update(self._format_cbs_report(report))
|
result.update(self._format_cbs_report(report))
|
||||||
elif self._attribute == 'check_control_messages':
|
elif self._attribute == "check_control_messages":
|
||||||
check_control_messages = vehicle_state.check_control_messages
|
check_control_messages = vehicle_state.check_control_messages
|
||||||
if not check_control_messages:
|
if not check_control_messages:
|
||||||
result['check_control_messages'] = 'OK'
|
result["check_control_messages"] = "OK"
|
||||||
else:
|
else:
|
||||||
result['check_control_messages'] = check_control_messages
|
result["check_control_messages"] = check_control_messages
|
||||||
elif self._attribute == 'charging_status':
|
elif self._attribute == "charging_status":
|
||||||
result['charging_status'] = vehicle_state.charging_status.value
|
result["charging_status"] = vehicle_state.charging_status.value
|
||||||
# pylint: disable=protected-access
|
# pylint: disable=protected-access
|
||||||
result['last_charging_end_result'] = \
|
result["last_charging_end_result"] = vehicle_state._attributes[
|
||||||
vehicle_state._attributes['lastChargingEndResult']
|
"lastChargingEndResult"
|
||||||
if self._attribute == 'connection_status':
|
]
|
||||||
|
if self._attribute == "connection_status":
|
||||||
# pylint: disable=protected-access
|
# pylint: disable=protected-access
|
||||||
result['connection_status'] = \
|
result["connection_status"] = vehicle_state._attributes["connectionStatus"]
|
||||||
vehicle_state._attributes['connectionStatus']
|
|
||||||
|
|
||||||
return sorted(result.items())
|
return sorted(result.items())
|
||||||
|
|
||||||
|
|
@ -141,49 +139,52 @@ class BMWConnectedDriveSensor(BinarySensorDevice):
|
||||||
"""Read new state data from the library."""
|
"""Read new state data from the library."""
|
||||||
from bimmer_connected.state import LockState
|
from bimmer_connected.state import LockState
|
||||||
from bimmer_connected.state import ChargingState
|
from bimmer_connected.state import ChargingState
|
||||||
|
|
||||||
vehicle_state = self._vehicle.state
|
vehicle_state = self._vehicle.state
|
||||||
|
|
||||||
# device class opening: On means open, Off means closed
|
# device class opening: On means open, Off means closed
|
||||||
if self._attribute == 'lids':
|
if self._attribute == "lids":
|
||||||
_LOGGER.debug("Status of lid: %s", vehicle_state.all_lids_closed)
|
_LOGGER.debug("Status of lid: %s", vehicle_state.all_lids_closed)
|
||||||
self._state = not vehicle_state.all_lids_closed
|
self._state = not vehicle_state.all_lids_closed
|
||||||
if self._attribute == 'windows':
|
if self._attribute == "windows":
|
||||||
self._state = not vehicle_state.all_windows_closed
|
self._state = not vehicle_state.all_windows_closed
|
||||||
# device class safety: On means unsafe, Off means safe
|
# device class safety: On means unsafe, Off means safe
|
||||||
if self._attribute == 'door_lock_state':
|
if self._attribute == "door_lock_state":
|
||||||
# Possible values: LOCKED, SECURED, SELECTIVE_LOCKED, UNLOCKED
|
# Possible values: LOCKED, SECURED, SELECTIVE_LOCKED, UNLOCKED
|
||||||
self._state = vehicle_state.door_lock_state not in \
|
self._state = vehicle_state.door_lock_state not in [
|
||||||
[LockState.LOCKED, LockState.SECURED]
|
LockState.LOCKED,
|
||||||
|
LockState.SECURED,
|
||||||
|
]
|
||||||
# device class light: On means light detected, Off means no light
|
# device class light: On means light detected, Off means no light
|
||||||
if self._attribute == 'lights_parking':
|
if self._attribute == "lights_parking":
|
||||||
self._state = vehicle_state.are_parking_lights_on
|
self._state = vehicle_state.are_parking_lights_on
|
||||||
# device class problem: On means problem detected, Off means no problem
|
# device class problem: On means problem detected, Off means no problem
|
||||||
if self._attribute == 'condition_based_services':
|
if self._attribute == "condition_based_services":
|
||||||
self._state = not vehicle_state.are_all_cbs_ok
|
self._state = not vehicle_state.are_all_cbs_ok
|
||||||
if self._attribute == 'check_control_messages':
|
if self._attribute == "check_control_messages":
|
||||||
self._state = vehicle_state.has_check_control_messages
|
self._state = vehicle_state.has_check_control_messages
|
||||||
# device class power: On means power detected, Off means no power
|
# device class power: On means power detected, Off means no power
|
||||||
if self._attribute == 'charging_status':
|
if self._attribute == "charging_status":
|
||||||
self._state = vehicle_state.charging_status in \
|
self._state = vehicle_state.charging_status in [ChargingState.CHARGING]
|
||||||
[ChargingState.CHARGING]
|
|
||||||
# device class plug: On means device is plugged in,
|
# device class plug: On means device is plugged in,
|
||||||
# Off means device is unplugged
|
# Off means device is unplugged
|
||||||
if self._attribute == 'connection_status':
|
if self._attribute == "connection_status":
|
||||||
# pylint: disable=protected-access
|
# pylint: disable=protected-access
|
||||||
self._state = (vehicle_state._attributes['connectionStatus'] ==
|
self._state = vehicle_state._attributes["connectionStatus"] == "CONNECTED"
|
||||||
'CONNECTED')
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _format_cbs_report(report):
|
def _format_cbs_report(report):
|
||||||
result = {}
|
result = {}
|
||||||
service_type = report.service_type.lower().replace('_', ' ')
|
service_type = report.service_type.lower().replace("_", " ")
|
||||||
result['{} status'.format(service_type)] = report.state.value
|
result["{} status".format(service_type)] = report.state.value
|
||||||
if report.due_date is not None:
|
if report.due_date is not None:
|
||||||
result['{} date'.format(service_type)] = \
|
result["{} date".format(service_type)] = report.due_date.strftime(
|
||||||
report.due_date.strftime('%Y-%m-%d')
|
"%Y-%m-%d"
|
||||||
|
)
|
||||||
if report.due_distance is not None:
|
if report.due_distance is not None:
|
||||||
result['{} distance'.format(service_type)] = \
|
result["{} distance".format(service_type)] = "{} km".format(
|
||||||
'{} km'.format(report.due_distance)
|
report.due_distance
|
||||||
|
)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def update_callback(self):
|
def update_callback(self):
|
||||||
|
|
|
||||||
|
|
@ -11,33 +11,42 @@ import voluptuous as vol
|
||||||
|
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.components.binary_sensor import (
|
from homeassistant.components.binary_sensor import (
|
||||||
BinarySensorDevice, DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA)
|
BinarySensorDevice,
|
||||||
|
DEVICE_CLASSES_SCHEMA,
|
||||||
|
PLATFORM_SCHEMA,
|
||||||
|
)
|
||||||
from homeassistant.components.sensor.command_line import CommandSensorData
|
from homeassistant.components.sensor.command_line import CommandSensorData
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON, CONF_NAME, CONF_VALUE_TEMPLATE,
|
CONF_PAYLOAD_OFF,
|
||||||
CONF_COMMAND, CONF_DEVICE_CLASS)
|
CONF_PAYLOAD_ON,
|
||||||
|
CONF_NAME,
|
||||||
|
CONF_VALUE_TEMPLATE,
|
||||||
|
CONF_COMMAND,
|
||||||
|
CONF_DEVICE_CLASS,
|
||||||
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DEFAULT_NAME = 'Binary Command Sensor'
|
DEFAULT_NAME = "Binary Command Sensor"
|
||||||
DEFAULT_PAYLOAD_ON = 'ON'
|
DEFAULT_PAYLOAD_ON = "ON"
|
||||||
DEFAULT_PAYLOAD_OFF = 'OFF'
|
DEFAULT_PAYLOAD_OFF = "OFF"
|
||||||
|
|
||||||
SCAN_INTERVAL = timedelta(seconds=60)
|
SCAN_INTERVAL = timedelta(seconds=60)
|
||||||
|
|
||||||
CONF_COMMAND_TIMEOUT = 'command_timeout'
|
CONF_COMMAND_TIMEOUT = "command_timeout"
|
||||||
DEFAULT_TIMEOUT = 15
|
DEFAULT_TIMEOUT = 15
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||||
vol.Required(CONF_COMMAND): cv.string,
|
{
|
||||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
vol.Required(CONF_COMMAND): cv.string,
|
||||||
vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string,
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||||
vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string,
|
vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string,
|
||||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string,
|
||||||
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
|
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||||
vol.Optional(
|
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
|
||||||
CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
|
vol.Optional(CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
|
|
@ -53,16 +62,22 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
value_template.hass = hass
|
value_template.hass = hass
|
||||||
data = CommandSensorData(hass, command, command_timeout)
|
data = CommandSensorData(hass, command, command_timeout)
|
||||||
|
|
||||||
add_entities([CommandBinarySensor(
|
add_entities(
|
||||||
hass, data, name, device_class, payload_on, payload_off,
|
[
|
||||||
value_template)], True)
|
CommandBinarySensor(
|
||||||
|
hass, data, name, device_class, payload_on, payload_off, value_template
|
||||||
|
)
|
||||||
|
],
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class CommandBinarySensor(BinarySensorDevice):
|
class CommandBinarySensor(BinarySensorDevice):
|
||||||
"""Representation of a command line binary sensor."""
|
"""Representation of a command line binary sensor."""
|
||||||
|
|
||||||
def __init__(self, hass, data, name, device_class, payload_on,
|
def __init__(
|
||||||
payload_off, value_template):
|
self, hass, data, name, device_class, payload_on, payload_off, value_template
|
||||||
|
):
|
||||||
"""Initialize the Command line binary sensor."""
|
"""Initialize the Command line binary sensor."""
|
||||||
self._hass = hass
|
self._hass = hass
|
||||||
self.data = data
|
self.data = data
|
||||||
|
|
@ -83,7 +98,7 @@ class CommandBinarySensor(BinarySensorDevice):
|
||||||
"""Return true if the binary sensor is on."""
|
"""Return true if the binary sensor is on."""
|
||||||
return self._state
|
return self._state
|
||||||
|
|
||||||
@ property
|
@property
|
||||||
def device_class(self):
|
def device_class(self):
|
||||||
"""Return the class of the binary sensor."""
|
"""Return the class of the binary sensor."""
|
||||||
return self._device_class
|
return self._device_class
|
||||||
|
|
@ -94,8 +109,7 @@ class CommandBinarySensor(BinarySensorDevice):
|
||||||
value = self.data.value
|
value = self.data.value
|
||||||
|
|
||||||
if self._value_template is not None:
|
if self._value_template is not None:
|
||||||
value = self._value_template.render_with_possible_json_value(
|
value = self._value_template.render_with_possible_json_value(value, False)
|
||||||
value, False)
|
|
||||||
if value == self._payload_on:
|
if value == self._payload_on:
|
||||||
self._state = True
|
self._state = True
|
||||||
elif value == self._payload_off:
|
elif value == self._payload_off:
|
||||||
|
|
|
||||||
|
|
@ -11,35 +11,39 @@ import requests
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import (
|
from homeassistant.components.binary_sensor import (
|
||||||
BinarySensorDevice, PLATFORM_SCHEMA, DEVICE_CLASSES)
|
BinarySensorDevice,
|
||||||
from homeassistant.const import (CONF_HOST, CONF_PORT)
|
PLATFORM_SCHEMA,
|
||||||
|
DEVICE_CLASSES,
|
||||||
|
)
|
||||||
|
from homeassistant.const import CONF_HOST, CONF_PORT
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
REQUIREMENTS = ['concord232==0.15']
|
REQUIREMENTS = ["concord232==0.15"]
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
CONF_EXCLUDE_ZONES = 'exclude_zones'
|
CONF_EXCLUDE_ZONES = "exclude_zones"
|
||||||
CONF_ZONE_TYPES = 'zone_types'
|
CONF_ZONE_TYPES = "zone_types"
|
||||||
|
|
||||||
DEFAULT_HOST = 'localhost'
|
DEFAULT_HOST = "localhost"
|
||||||
DEFAULT_NAME = 'Alarm'
|
DEFAULT_NAME = "Alarm"
|
||||||
DEFAULT_PORT = '5007'
|
DEFAULT_PORT = "5007"
|
||||||
DEFAULT_SSL = False
|
DEFAULT_SSL = False
|
||||||
|
|
||||||
SCAN_INTERVAL = datetime.timedelta(seconds=10)
|
SCAN_INTERVAL = datetime.timedelta(seconds=10)
|
||||||
|
|
||||||
ZONE_TYPES_SCHEMA = vol.Schema({
|
ZONE_TYPES_SCHEMA = vol.Schema({cv.positive_int: vol.In(DEVICE_CLASSES)})
|
||||||
cv.positive_int: vol.In(DEVICE_CLASSES),
|
|
||||||
})
|
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||||
vol.Optional(CONF_EXCLUDE_ZONES, default=[]):
|
{
|
||||||
vol.All(cv.ensure_list, [cv.positive_int]),
|
vol.Optional(CONF_EXCLUDE_ZONES, default=[]): vol.All(
|
||||||
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
|
cv.ensure_list, [cv.positive_int]
|
||||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
),
|
||||||
vol.Optional(CONF_ZONE_TYPES, default={}): ZONE_TYPES_SCHEMA,
|
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
|
||||||
})
|
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||||
|
vol.Optional(CONF_ZONE_TYPES, default={}): ZONE_TYPES_SCHEMA,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
|
|
@ -54,7 +58,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
_LOGGER.debug("Initializing client")
|
_LOGGER.debug("Initializing client")
|
||||||
client = concord232_client.Client('http://{}:{}'.format(host, port))
|
client = concord232_client.Client("http://{}:{}".format(host, port))
|
||||||
client.zones = client.list_zones()
|
client.zones = client.list_zones()
|
||||||
client.last_zone_update = datetime.datetime.now()
|
client.last_zone_update = datetime.datetime.now()
|
||||||
|
|
||||||
|
|
@ -67,15 +71,17 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
# name mapping to different sensors in an unpredictable way. Sort
|
# name mapping to different sensors in an unpredictable way. Sort
|
||||||
# the zones by zone number to prevent this.
|
# the zones by zone number to prevent this.
|
||||||
|
|
||||||
client.zones.sort(key=lambda zone: zone['number'])
|
client.zones.sort(key=lambda zone: zone["number"])
|
||||||
|
|
||||||
for zone in client.zones:
|
for zone in client.zones:
|
||||||
_LOGGER.info("Loading Zone found: %s", zone['name'])
|
_LOGGER.info("Loading Zone found: %s", zone["name"])
|
||||||
if zone['number'] not in exclude:
|
if zone["number"] not in exclude:
|
||||||
sensors.append(
|
sensors.append(
|
||||||
Concord232ZoneSensor(
|
Concord232ZoneSensor(
|
||||||
hass, client, zone, zone_types.get(
|
hass,
|
||||||
zone['number'], get_opening_type(zone))
|
client,
|
||||||
|
zone,
|
||||||
|
zone_types.get(zone["number"], get_opening_type(zone)),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -84,15 +90,15 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
|
|
||||||
def get_opening_type(zone):
|
def get_opening_type(zone):
|
||||||
"""Return the result of the type guessing from name."""
|
"""Return the result of the type guessing from name."""
|
||||||
if 'MOTION' in zone['name']:
|
if "MOTION" in zone["name"]:
|
||||||
return 'motion'
|
return "motion"
|
||||||
if 'KEY' in zone['name']:
|
if "KEY" in zone["name"]:
|
||||||
return 'safety'
|
return "safety"
|
||||||
if 'SMOKE' in zone['name']:
|
if "SMOKE" in zone["name"]:
|
||||||
return 'smoke'
|
return "smoke"
|
||||||
if 'WATER' in zone['name']:
|
if "WATER" in zone["name"]:
|
||||||
return 'water'
|
return "water"
|
||||||
return 'opening'
|
return "opening"
|
||||||
|
|
||||||
|
|
||||||
class Concord232ZoneSensor(BinarySensorDevice):
|
class Concord232ZoneSensor(BinarySensorDevice):
|
||||||
|
|
@ -103,7 +109,7 @@ class Concord232ZoneSensor(BinarySensorDevice):
|
||||||
self._hass = hass
|
self._hass = hass
|
||||||
self._client = client
|
self._client = client
|
||||||
self._zone = zone
|
self._zone = zone
|
||||||
self._number = zone['number']
|
self._number = zone["number"]
|
||||||
self._zone_type = zone_type
|
self._zone_type = zone_type
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
@ -119,13 +125,13 @@ class Concord232ZoneSensor(BinarySensorDevice):
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Return the name of the binary sensor."""
|
"""Return the name of the binary sensor."""
|
||||||
return self._zone['name']
|
return self._zone["name"]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self):
|
def is_on(self):
|
||||||
"""Return true if the binary sensor is on."""
|
"""Return true if the binary sensor is on."""
|
||||||
# True means "faulted" or "open" or "abnormal state"
|
# True means "faulted" or "open" or "abnormal state"
|
||||||
return bool(self._zone['state'] != 'Normal')
|
return bool(self._zone["state"] != "Normal")
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
"""Get updated stats from API."""
|
"""Get updated stats from API."""
|
||||||
|
|
@ -134,8 +140,9 @@ class Concord232ZoneSensor(BinarySensorDevice):
|
||||||
if last_update > datetime.timedelta(seconds=1):
|
if last_update > datetime.timedelta(seconds=1):
|
||||||
self._client.zones = self._client.list_zones()
|
self._client.zones = self._client.list_zones()
|
||||||
self._client.last_zone_update = datetime.datetime.now()
|
self._client.last_zone_update = datetime.datetime.now()
|
||||||
_LOGGER.debug("Updated from zone: %s", self._zone['name'])
|
_LOGGER.debug("Updated from zone: %s", self._zone["name"])
|
||||||
|
|
||||||
if hasattr(self._client, 'zones'):
|
if hasattr(self._client, "zones"):
|
||||||
self._zone = next((x for x in self._client.zones
|
self._zone = next(
|
||||||
if x['number'] == self._number), None)
|
(x for x in self._client.zones if x["number"] == self._number), None
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -6,38 +6,47 @@ https://home-assistant.io/components/binary_sensor.deconz/
|
||||||
"""
|
"""
|
||||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||||
from homeassistant.components.deconz.const import (
|
from homeassistant.components.deconz.const import (
|
||||||
ATTR_DARK, ATTR_ON, CONF_ALLOW_CLIP_SENSOR, DOMAIN as DATA_DECONZ,
|
ATTR_DARK,
|
||||||
DATA_DECONZ_ID, DATA_DECONZ_UNSUB, DECONZ_DOMAIN)
|
ATTR_ON,
|
||||||
|
CONF_ALLOW_CLIP_SENSOR,
|
||||||
|
DOMAIN as DATA_DECONZ,
|
||||||
|
DATA_DECONZ_ID,
|
||||||
|
DATA_DECONZ_UNSUB,
|
||||||
|
DECONZ_DOMAIN,
|
||||||
|
)
|
||||||
from homeassistant.const import ATTR_BATTERY_LEVEL
|
from homeassistant.const import ATTR_BATTERY_LEVEL
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE
|
from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
|
|
||||||
DEPENDENCIES = ['deconz']
|
DEPENDENCIES = ["deconz"]
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_platform(hass, config, async_add_entities,
|
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||||
discovery_info=None):
|
|
||||||
"""Old way of setting up deCONZ binary sensors."""
|
"""Old way of setting up deCONZ binary sensors."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
"""Set up the deCONZ binary sensor."""
|
"""Set up the deCONZ binary sensor."""
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_add_sensor(sensors):
|
def async_add_sensor(sensors):
|
||||||
"""Add binary sensor from deCONZ."""
|
"""Add binary sensor from deCONZ."""
|
||||||
from pydeconz.sensor import DECONZ_BINARY_SENSOR
|
from pydeconz.sensor import DECONZ_BINARY_SENSOR
|
||||||
|
|
||||||
entities = []
|
entities = []
|
||||||
allow_clip_sensor = config_entry.data.get(CONF_ALLOW_CLIP_SENSOR, True)
|
allow_clip_sensor = config_entry.data.get(CONF_ALLOW_CLIP_SENSOR, True)
|
||||||
for sensor in sensors:
|
for sensor in sensors:
|
||||||
if sensor.type in DECONZ_BINARY_SENSOR and \
|
if sensor.type in DECONZ_BINARY_SENSOR and not (
|
||||||
not (not allow_clip_sensor and sensor.type.startswith('CLIP')):
|
not allow_clip_sensor and sensor.type.startswith("CLIP")
|
||||||
|
):
|
||||||
entities.append(DeconzBinarySensor(sensor))
|
entities.append(DeconzBinarySensor(sensor))
|
||||||
async_add_entities(entities, True)
|
async_add_entities(entities, True)
|
||||||
|
|
||||||
hass.data[DATA_DECONZ_UNSUB].append(
|
hass.data[DATA_DECONZ_UNSUB].append(
|
||||||
async_dispatcher_connect(hass, 'deconz_new_sensor', async_add_sensor))
|
async_dispatcher_connect(hass, "deconz_new_sensor", async_add_sensor)
|
||||||
|
)
|
||||||
|
|
||||||
async_add_sensor(hass.data[DATA_DECONZ].sensors.values())
|
async_add_sensor(hass.data[DATA_DECONZ].sensors.values())
|
||||||
|
|
||||||
|
|
@ -66,10 +75,12 @@ class DeconzBinarySensor(BinarySensorDevice):
|
||||||
If reason is that state is updated,
|
If reason is that state is updated,
|
||||||
or reachable has changed or battery has changed.
|
or reachable has changed or battery has changed.
|
||||||
"""
|
"""
|
||||||
if reason['state'] or \
|
if (
|
||||||
'reachable' in reason['attr'] or \
|
reason["state"]
|
||||||
'battery' in reason['attr'] or \
|
or "reachable" in reason["attr"]
|
||||||
'on' in reason['attr']:
|
or "battery" in reason["attr"]
|
||||||
|
or "on" in reason["attr"]
|
||||||
|
):
|
||||||
self.async_schedule_update_ha_state()
|
self.async_schedule_update_ha_state()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
@ -111,6 +122,7 @@ class DeconzBinarySensor(BinarySensorDevice):
|
||||||
def device_state_attributes(self):
|
def device_state_attributes(self):
|
||||||
"""Return the state attributes of the sensor."""
|
"""Return the state attributes of the sensor."""
|
||||||
from pydeconz.sensor import PRESENCE
|
from pydeconz.sensor import PRESENCE
|
||||||
|
|
||||||
attr = {}
|
attr = {}
|
||||||
if self._sensor.battery:
|
if self._sensor.battery:
|
||||||
attr[ATTR_BATTERY_LEVEL] = self._sensor.battery
|
attr[ATTR_BATTERY_LEVEL] = self._sensor.battery
|
||||||
|
|
@ -123,15 +135,14 @@ class DeconzBinarySensor(BinarySensorDevice):
|
||||||
@property
|
@property
|
||||||
def device_info(self):
|
def device_info(self):
|
||||||
"""Return a device description for device registry."""
|
"""Return a device description for device registry."""
|
||||||
if (self._sensor.uniqueid is None or
|
if self._sensor.uniqueid is None or self._sensor.uniqueid.count(":") != 7:
|
||||||
self._sensor.uniqueid.count(':') != 7):
|
|
||||||
return None
|
return None
|
||||||
serial = self._sensor.uniqueid.split('-', 1)[0]
|
serial = self._sensor.uniqueid.split("-", 1)[0]
|
||||||
return {
|
return {
|
||||||
'connections': {(CONNECTION_ZIGBEE, serial)},
|
"connections": {(CONNECTION_ZIGBEE, serial)},
|
||||||
'identifiers': {(DECONZ_DOMAIN, serial)},
|
"identifiers": {(DECONZ_DOMAIN, serial)},
|
||||||
'manufacturer': self._sensor.manufacturer,
|
"manufacturer": self._sensor.manufacturer,
|
||||||
'model': self._sensor.modelid,
|
"model": self._sensor.modelid,
|
||||||
'name': self._sensor.name,
|
"name": self._sensor.name,
|
||||||
'sw_version': self._sensor.swversion,
|
"sw_version": self._sensor.swversion,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,12 @@ from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||||
|
|
||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
"""Set up the Demo binary sensor platform."""
|
"""Set up the Demo binary sensor platform."""
|
||||||
add_entities([
|
add_entities(
|
||||||
DemoBinarySensor('Basement Floor Wet', False, 'moisture'),
|
[
|
||||||
DemoBinarySensor('Movement Backyard', True, 'motion'),
|
DemoBinarySensor("Basement Floor Wet", False, "moisture"),
|
||||||
])
|
DemoBinarySensor("Movement Backyard", True, "motion"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class DemoBinarySensor(BinarySensorDevice):
|
class DemoBinarySensor(BinarySensorDevice):
|
||||||
|
|
|
||||||
|
|
@ -9,23 +9,32 @@ import logging
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.components.binary_sensor import (
|
from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA
|
||||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
|
||||||
from homeassistant.components.digital_ocean import (
|
from homeassistant.components.digital_ocean import (
|
||||||
CONF_DROPLETS, ATTR_CREATED_AT, ATTR_DROPLET_ID, ATTR_DROPLET_NAME,
|
CONF_DROPLETS,
|
||||||
ATTR_FEATURES, ATTR_IPV4_ADDRESS, ATTR_IPV6_ADDRESS, ATTR_MEMORY,
|
ATTR_CREATED_AT,
|
||||||
ATTR_REGION, ATTR_VCPUS, CONF_ATTRIBUTION, DATA_DIGITAL_OCEAN)
|
ATTR_DROPLET_ID,
|
||||||
|
ATTR_DROPLET_NAME,
|
||||||
|
ATTR_FEATURES,
|
||||||
|
ATTR_IPV4_ADDRESS,
|
||||||
|
ATTR_IPV6_ADDRESS,
|
||||||
|
ATTR_MEMORY,
|
||||||
|
ATTR_REGION,
|
||||||
|
ATTR_VCPUS,
|
||||||
|
CONF_ATTRIBUTION,
|
||||||
|
DATA_DIGITAL_OCEAN,
|
||||||
|
)
|
||||||
from homeassistant.const import ATTR_ATTRIBUTION
|
from homeassistant.const import ATTR_ATTRIBUTION
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DEFAULT_NAME = 'Droplet'
|
DEFAULT_NAME = "Droplet"
|
||||||
DEFAULT_DEVICE_CLASS = 'moving'
|
DEFAULT_DEVICE_CLASS = "moving"
|
||||||
DEPENDENCIES = ['digital_ocean']
|
DEPENDENCIES = ["digital_ocean"]
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||||
vol.Required(CONF_DROPLETS): vol.All(cv.ensure_list, [cv.string]),
|
{vol.Required(CONF_DROPLETS): vol.All(cv.ensure_list, [cv.string])}
|
||||||
})
|
)
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
|
|
@ -65,7 +74,7 @@ class DigitalOceanBinarySensor(BinarySensorDevice):
|
||||||
@property
|
@property
|
||||||
def is_on(self):
|
def is_on(self):
|
||||||
"""Return true if the binary sensor is on."""
|
"""Return true if the binary sensor is on."""
|
||||||
return self.data.status == 'active'
|
return self.data.status == "active"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device_class(self):
|
def device_class(self):
|
||||||
|
|
@ -84,7 +93,7 @@ class DigitalOceanBinarySensor(BinarySensorDevice):
|
||||||
ATTR_IPV4_ADDRESS: self.data.ip_address,
|
ATTR_IPV4_ADDRESS: self.data.ip_address,
|
||||||
ATTR_IPV6_ADDRESS: self.data.ip_v6_address,
|
ATTR_IPV6_ADDRESS: self.data.ip_v6_address,
|
||||||
ATTR_MEMORY: self.data.memory,
|
ATTR_MEMORY: self.data.memory,
|
||||||
ATTR_REGION: self.data.region['name'],
|
ATTR_REGION: self.data.region["name"],
|
||||||
ATTR_VCPUS: self.data.vcpus,
|
ATTR_VCPUS: self.data.vcpus,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,9 @@ https://home-assistant.io/components/binary_sensor.ecobee/
|
||||||
from homeassistant.components import ecobee
|
from homeassistant.components import ecobee
|
||||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||||
|
|
||||||
DEPENDENCIES = ['ecobee']
|
DEPENDENCIES = ["ecobee"]
|
||||||
|
|
||||||
ECOBEE_CONFIG_FILE = 'ecobee.conf'
|
ECOBEE_CONFIG_FILE = "ecobee.conf"
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
|
|
@ -20,11 +20,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
dev = list()
|
dev = list()
|
||||||
for index in range(len(data.ecobee.thermostats)):
|
for index in range(len(data.ecobee.thermostats)):
|
||||||
for sensor in data.ecobee.get_remote_sensors(index):
|
for sensor in data.ecobee.get_remote_sensors(index):
|
||||||
for item in sensor['capability']:
|
for item in sensor["capability"]:
|
||||||
if item['type'] != 'occupancy':
|
if item["type"] != "occupancy":
|
||||||
continue
|
continue
|
||||||
|
|
||||||
dev.append(EcobeeBinarySensor(sensor['name'], index))
|
dev.append(EcobeeBinarySensor(sensor["name"], index))
|
||||||
|
|
||||||
add_entities(dev, True)
|
add_entities(dev, True)
|
||||||
|
|
||||||
|
|
@ -34,11 +34,11 @@ class EcobeeBinarySensor(BinarySensorDevice):
|
||||||
|
|
||||||
def __init__(self, sensor_name, sensor_index):
|
def __init__(self, sensor_name, sensor_index):
|
||||||
"""Initialize the sensor."""
|
"""Initialize the sensor."""
|
||||||
self._name = sensor_name + ' Occupancy'
|
self._name = sensor_name + " Occupancy"
|
||||||
self.sensor_name = sensor_name
|
self.sensor_name = sensor_name
|
||||||
self.index = sensor_index
|
self.index = sensor_index
|
||||||
self._state = None
|
self._state = None
|
||||||
self._device_class = 'occupancy'
|
self._device_class = "occupancy"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
|
|
@ -48,7 +48,7 @@ class EcobeeBinarySensor(BinarySensorDevice):
|
||||||
@property
|
@property
|
||||||
def is_on(self):
|
def is_on(self):
|
||||||
"""Return the status of the sensor."""
|
"""Return the status of the sensor."""
|
||||||
return self._state == 'true'
|
return self._state == "true"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device_class(self):
|
def device_class(self):
|
||||||
|
|
@ -60,7 +60,6 @@ class EcobeeBinarySensor(BinarySensorDevice):
|
||||||
data = ecobee.NETWORK
|
data = ecobee.NETWORK
|
||||||
data.update()
|
data.update()
|
||||||
for sensor in data.ecobee.get_remote_sensors(self.index):
|
for sensor in data.ecobee.get_remote_sensors(self.index):
|
||||||
for item in sensor['capability']:
|
for item in sensor["capability"]:
|
||||||
if (item['type'] == 'occupancy' and
|
if item["type"] == "occupancy" and self.sensor_name == sensor["name"]:
|
||||||
self.sensor_name == sensor['name']):
|
self._state = item["value"]
|
||||||
self._state = item['value']
|
|
||||||
|
|
|
||||||
|
|
@ -9,21 +9,21 @@ import logging
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||||
from homeassistant.const import STATE_ON, STATE_OFF
|
from homeassistant.const import STATE_ON, STATE_OFF
|
||||||
from homeassistant.components.egardia import (
|
from homeassistant.components.egardia import EGARDIA_DEVICE, ATTR_DISCOVER_DEVICES
|
||||||
EGARDIA_DEVICE, ATTR_DISCOVER_DEVICES)
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
DEPENDENCIES = ['egardia']
|
DEPENDENCIES = ["egardia"]
|
||||||
EGARDIA_TYPE_TO_DEVICE_CLASS = {'IR Sensor': 'motion',
|
EGARDIA_TYPE_TO_DEVICE_CLASS = {
|
||||||
'Door Contact': 'opening',
|
"IR Sensor": "motion",
|
||||||
'IR': 'motion'}
|
"Door Contact": "opening",
|
||||||
|
"IR": "motion",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_setup_platform(hass, config, async_add_entities,
|
def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||||
discovery_info=None):
|
|
||||||
"""Initialize the platform."""
|
"""Initialize the platform."""
|
||||||
if (discovery_info is None or
|
if discovery_info is None or discovery_info[ATTR_DISCOVER_DEVICES] is None:
|
||||||
discovery_info[ATTR_DISCOVER_DEVICES] is None):
|
|
||||||
return
|
return
|
||||||
|
|
||||||
disc_info = discovery_info[ATTR_DISCOVER_DEVICES]
|
disc_info = discovery_info[ATTR_DISCOVER_DEVICES]
|
||||||
|
|
@ -31,14 +31,17 @@ def async_setup_platform(hass, config, async_add_entities,
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
(
|
(
|
||||||
EgardiaBinarySensor(
|
EgardiaBinarySensor(
|
||||||
sensor_id=disc_info[sensor]['id'],
|
sensor_id=disc_info[sensor]["id"],
|
||||||
name=disc_info[sensor]['name'],
|
name=disc_info[sensor]["name"],
|
||||||
egardia_system=hass.data[EGARDIA_DEVICE],
|
egardia_system=hass.data[EGARDIA_DEVICE],
|
||||||
device_class=EGARDIA_TYPE_TO_DEVICE_CLASS.get(
|
device_class=EGARDIA_TYPE_TO_DEVICE_CLASS.get(
|
||||||
disc_info[sensor]['type'], None)
|
disc_info[sensor]["type"], None
|
||||||
|
),
|
||||||
)
|
)
|
||||||
for sensor in disc_info
|
for sensor in disc_info
|
||||||
), True)
|
),
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class EgardiaBinarySensor(BinarySensorDevice):
|
class EgardiaBinarySensor(BinarySensorDevice):
|
||||||
|
|
|
||||||
|
|
@ -8,20 +8,23 @@ import logging
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||||
from homeassistant.components.eight_sleep import (
|
from homeassistant.components.eight_sleep import (
|
||||||
DATA_EIGHT, EightSleepHeatEntity, CONF_BINARY_SENSORS, NAME_MAP)
|
DATA_EIGHT,
|
||||||
|
EightSleepHeatEntity,
|
||||||
|
CONF_BINARY_SENSORS,
|
||||||
|
NAME_MAP,
|
||||||
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DEPENDENCIES = ['eight_sleep']
|
DEPENDENCIES = ["eight_sleep"]
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_platform(hass, config, async_add_entities,
|
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||||
discovery_info=None):
|
|
||||||
"""Set up the eight sleep binary sensor."""
|
"""Set up the eight sleep binary sensor."""
|
||||||
if discovery_info is None:
|
if discovery_info is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
name = 'Eight'
|
name = "Eight"
|
||||||
sensors = discovery_info[CONF_BINARY_SENSORS]
|
sensors = discovery_info[CONF_BINARY_SENSORS]
|
||||||
eight = hass.data[DATA_EIGHT]
|
eight = hass.data[DATA_EIGHT]
|
||||||
|
|
||||||
|
|
@ -42,15 +45,19 @@ class EightHeatSensor(EightSleepHeatEntity, BinarySensorDevice):
|
||||||
|
|
||||||
self._sensor = sensor
|
self._sensor = sensor
|
||||||
self._mapped_name = NAME_MAP.get(self._sensor, self._sensor)
|
self._mapped_name = NAME_MAP.get(self._sensor, self._sensor)
|
||||||
self._name = '{} {}'.format(name, self._mapped_name)
|
self._name = "{} {}".format(name, self._mapped_name)
|
||||||
self._state = None
|
self._state = None
|
||||||
|
|
||||||
self._side = self._sensor.split('_')[0]
|
self._side = self._sensor.split("_")[0]
|
||||||
self._userid = self._eight.fetch_userid(self._side)
|
self._userid = self._eight.fetch_userid(self._side)
|
||||||
self._usrobj = self._eight.users[self._userid]
|
self._usrobj = self._eight.users[self._userid]
|
||||||
|
|
||||||
_LOGGER.debug("Presence Sensor: %s, Side: %s, User: %s",
|
_LOGGER.debug(
|
||||||
self._sensor, self._side, self._userid)
|
"Presence Sensor: %s, Side: %s, User: %s",
|
||||||
|
self._sensor,
|
||||||
|
self._side,
|
||||||
|
self._userid,
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
|
|
|
||||||
|
|
@ -9,22 +9,26 @@ import logging
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import (
|
from homeassistant.components.binary_sensor import (
|
||||||
BinarySensorDevice, PLATFORM_SCHEMA, DEVICE_CLASSES_SCHEMA)
|
BinarySensorDevice,
|
||||||
|
PLATFORM_SCHEMA,
|
||||||
|
DEVICE_CLASSES_SCHEMA,
|
||||||
|
)
|
||||||
from homeassistant.components import enocean
|
from homeassistant.components import enocean
|
||||||
from homeassistant.const import (
|
from homeassistant.const import CONF_NAME, CONF_ID, CONF_DEVICE_CLASS
|
||||||
CONF_NAME, CONF_ID, CONF_DEVICE_CLASS)
|
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DEPENDENCIES = ['enocean']
|
DEPENDENCIES = ["enocean"]
|
||||||
DEFAULT_NAME = 'EnOcean binary sensor'
|
DEFAULT_NAME = "EnOcean binary sensor"
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||||
vol.Required(CONF_ID): vol.All(cv.ensure_list, [vol.Coerce(int)]),
|
{
|
||||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
vol.Required(CONF_ID): vol.All(cv.ensure_list, [vol.Coerce(int)]),
|
||||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||||
})
|
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
|
|
@ -42,7 +46,7 @@ class EnOceanBinarySensor(enocean.EnOceanDevice, BinarySensorDevice):
|
||||||
def __init__(self, dev_id, devname, device_class):
|
def __init__(self, dev_id, devname, device_class):
|
||||||
"""Initialize the EnOcean binary sensor."""
|
"""Initialize the EnOcean binary sensor."""
|
||||||
enocean.EnOceanDevice.__init__(self)
|
enocean.EnOceanDevice.__init__(self)
|
||||||
self.stype = 'listener'
|
self.stype = "listener"
|
||||||
self.dev_id = dev_id
|
self.dev_id = dev_id
|
||||||
self.which = -1
|
self.which = -1
|
||||||
self.onoff = -1
|
self.onoff = -1
|
||||||
|
|
@ -84,7 +88,12 @@ class EnOceanBinarySensor(enocean.EnOceanDevice, BinarySensorDevice):
|
||||||
elif value2 == 0x15:
|
elif value2 == 0x15:
|
||||||
self.which = 10
|
self.which = 10
|
||||||
self.onoff = 1
|
self.onoff = 1
|
||||||
self.hass.bus.fire('button_pressed', {'id': self.dev_id,
|
self.hass.bus.fire(
|
||||||
'pushed': value,
|
"button_pressed",
|
||||||
'which': self.which,
|
{
|
||||||
'onoff': self.onoff})
|
"id": self.dev_id,
|
||||||
|
"pushed": value,
|
||||||
|
"which": self.which,
|
||||||
|
"onoff": self.onoff,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -12,21 +12,25 @@ from homeassistant.core import callback
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||||
from homeassistant.components.envisalink import (
|
from homeassistant.components.envisalink import (
|
||||||
DATA_EVL, ZONE_SCHEMA, CONF_ZONENAME, CONF_ZONETYPE, EnvisalinkDevice,
|
DATA_EVL,
|
||||||
SIGNAL_ZONE_UPDATE)
|
ZONE_SCHEMA,
|
||||||
|
CONF_ZONENAME,
|
||||||
|
CONF_ZONETYPE,
|
||||||
|
EnvisalinkDevice,
|
||||||
|
SIGNAL_ZONE_UPDATE,
|
||||||
|
)
|
||||||
from homeassistant.const import ATTR_LAST_TRIP_TIME
|
from homeassistant.const import ATTR_LAST_TRIP_TIME
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DEPENDENCIES = ['envisalink']
|
DEPENDENCIES = ["envisalink"]
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_setup_platform(hass, config, async_add_entities,
|
def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||||
discovery_info=None):
|
|
||||||
"""Set up the Envisalink binary sensor devices."""
|
"""Set up the Envisalink binary sensor devices."""
|
||||||
configured_zones = discovery_info['zones']
|
configured_zones = discovery_info["zones"]
|
||||||
|
|
||||||
devices = []
|
devices = []
|
||||||
for zone_num in configured_zones:
|
for zone_num in configured_zones:
|
||||||
|
|
@ -36,8 +40,8 @@ def async_setup_platform(hass, config, async_add_entities,
|
||||||
zone_num,
|
zone_num,
|
||||||
device_config_data[CONF_ZONENAME],
|
device_config_data[CONF_ZONENAME],
|
||||||
device_config_data[CONF_ZONETYPE],
|
device_config_data[CONF_ZONETYPE],
|
||||||
hass.data[DATA_EVL].alarm_state['zone'][zone_num],
|
hass.data[DATA_EVL].alarm_state["zone"][zone_num],
|
||||||
hass.data[DATA_EVL]
|
hass.data[DATA_EVL],
|
||||||
)
|
)
|
||||||
devices.append(device)
|
devices.append(device)
|
||||||
|
|
||||||
|
|
@ -47,20 +51,18 @@ def async_setup_platform(hass, config, async_add_entities,
|
||||||
class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice):
|
class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice):
|
||||||
"""Representation of an Envisalink binary sensor."""
|
"""Representation of an Envisalink binary sensor."""
|
||||||
|
|
||||||
def __init__(self, hass, zone_number, zone_name, zone_type, info,
|
def __init__(self, hass, zone_number, zone_name, zone_type, info, controller):
|
||||||
controller):
|
|
||||||
"""Initialize the binary_sensor."""
|
"""Initialize the binary_sensor."""
|
||||||
self._zone_type = zone_type
|
self._zone_type = zone_type
|
||||||
self._zone_number = zone_number
|
self._zone_number = zone_number
|
||||||
|
|
||||||
_LOGGER.debug('Setting up zone: %s', zone_name)
|
_LOGGER.debug("Setting up zone: %s", zone_name)
|
||||||
super().__init__(zone_name, info, controller)
|
super().__init__(zone_name, info, controller)
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_added_to_hass(self):
|
def async_added_to_hass(self):
|
||||||
"""Register callbacks."""
|
"""Register callbacks."""
|
||||||
async_dispatcher_connect(
|
async_dispatcher_connect(self.hass, SIGNAL_ZONE_UPDATE, self._update_callback)
|
||||||
self.hass, SIGNAL_ZONE_UPDATE, self._update_callback)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device_state_attributes(self):
|
def device_state_attributes(self):
|
||||||
|
|
@ -76,7 +78,7 @@ class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice):
|
||||||
# interval, so we subtract it from the current second-accurate time
|
# interval, so we subtract it from the current second-accurate time
|
||||||
# unless it is already at the maximum value, in which case we set it
|
# unless it is already at the maximum value, in which case we set it
|
||||||
# to None since we can't determine the actual value.
|
# to None since we can't determine the actual value.
|
||||||
seconds_ago = self._info['last_fault']
|
seconds_ago = self._info["last_fault"]
|
||||||
if seconds_ago < 65536 * 5:
|
if seconds_ago < 65536 * 5:
|
||||||
now = dt_util.now().replace(microsecond=0)
|
now = dt_util.now().replace(microsecond=0)
|
||||||
delta = datetime.timedelta(seconds=seconds_ago)
|
delta = datetime.timedelta(seconds=seconds_ago)
|
||||||
|
|
@ -90,7 +92,7 @@ class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice):
|
||||||
@property
|
@property
|
||||||
def is_on(self):
|
def is_on(self):
|
||||||
"""Return true if sensor is on."""
|
"""Return true if sensor is on."""
|
||||||
return self._info['status']['open']
|
return self._info["status"]["open"]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device_class(self):
|
def device_class(self):
|
||||||
|
|
|
||||||
|
|
@ -11,44 +11,52 @@ import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.components.binary_sensor import (
|
from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA
|
||||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
|
||||||
from homeassistant.components.ffmpeg import (
|
from homeassistant.components.ffmpeg import (
|
||||||
FFmpegBase, DATA_FFMPEG, CONF_INPUT, CONF_EXTRA_ARGUMENTS,
|
FFmpegBase,
|
||||||
CONF_INITIAL_STATE)
|
DATA_FFMPEG,
|
||||||
|
CONF_INPUT,
|
||||||
|
CONF_EXTRA_ARGUMENTS,
|
||||||
|
CONF_INITIAL_STATE,
|
||||||
|
)
|
||||||
from homeassistant.const import CONF_NAME
|
from homeassistant.const import CONF_NAME
|
||||||
|
|
||||||
DEPENDENCIES = ['ffmpeg']
|
DEPENDENCIES = ["ffmpeg"]
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
CONF_RESET = 'reset'
|
CONF_RESET = "reset"
|
||||||
CONF_CHANGES = 'changes'
|
CONF_CHANGES = "changes"
|
||||||
CONF_REPEAT = 'repeat'
|
CONF_REPEAT = "repeat"
|
||||||
CONF_REPEAT_TIME = 'repeat_time'
|
CONF_REPEAT_TIME = "repeat_time"
|
||||||
|
|
||||||
DEFAULT_NAME = 'FFmpeg Motion'
|
DEFAULT_NAME = "FFmpeg Motion"
|
||||||
DEFAULT_INIT_STATE = True
|
DEFAULT_INIT_STATE = True
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||||
vol.Required(CONF_INPUT): cv.string,
|
{
|
||||||
vol.Optional(CONF_INITIAL_STATE, default=DEFAULT_INIT_STATE): cv.boolean,
|
vol.Required(CONF_INPUT): cv.string,
|
||||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
vol.Optional(CONF_INITIAL_STATE, default=DEFAULT_INIT_STATE): cv.boolean,
|
||||||
vol.Optional(CONF_EXTRA_ARGUMENTS): cv.string,
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||||
vol.Optional(CONF_RESET, default=10):
|
vol.Optional(CONF_EXTRA_ARGUMENTS): cv.string,
|
||||||
vol.All(vol.Coerce(int), vol.Range(min=1)),
|
vol.Optional(CONF_RESET, default=10): vol.All(
|
||||||
vol.Optional(CONF_CHANGES, default=10):
|
vol.Coerce(int), vol.Range(min=1)
|
||||||
vol.All(vol.Coerce(float), vol.Range(min=0, max=99)),
|
),
|
||||||
vol.Inclusive(CONF_REPEAT, 'repeat'):
|
vol.Optional(CONF_CHANGES, default=10): vol.All(
|
||||||
vol.All(vol.Coerce(int), vol.Range(min=1)),
|
vol.Coerce(float), vol.Range(min=0, max=99)
|
||||||
vol.Inclusive(CONF_REPEAT_TIME, 'repeat'):
|
),
|
||||||
vol.All(vol.Coerce(int), vol.Range(min=1)),
|
vol.Inclusive(CONF_REPEAT, "repeat"): vol.All(
|
||||||
})
|
vol.Coerce(int), vol.Range(min=1)
|
||||||
|
),
|
||||||
|
vol.Inclusive(CONF_REPEAT_TIME, "repeat"): vol.All(
|
||||||
|
vol.Coerce(int), vol.Range(min=1)
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_setup_platform(hass, config, async_add_entities,
|
def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||||
discovery_info=None):
|
|
||||||
"""Set up the FFmpeg binary motion sensor."""
|
"""Set up the FFmpeg binary motion sensor."""
|
||||||
manager = hass.data[DATA_FFMPEG]
|
manager = hass.data[DATA_FFMPEG]
|
||||||
|
|
||||||
|
|
@ -95,8 +103,7 @@ class FFmpegMotion(FFmpegBinarySensor):
|
||||||
from haffmpeg import SensorMotion
|
from haffmpeg import SensorMotion
|
||||||
|
|
||||||
super().__init__(config)
|
super().__init__(config)
|
||||||
self.ffmpeg = SensorMotion(
|
self.ffmpeg = SensorMotion(manager.binary, hass.loop, self._async_callback)
|
||||||
manager.binary, hass.loop, self._async_callback)
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def _async_start_ffmpeg(self, entity_ids):
|
def _async_start_ffmpeg(self, entity_ids):
|
||||||
|
|
@ -124,4 +131,4 @@ class FFmpegMotion(FFmpegBinarySensor):
|
||||||
@property
|
@property
|
||||||
def device_class(self):
|
def device_class(self):
|
||||||
"""Return the class of this sensor, from DEVICE_CLASSES."""
|
"""Return the class of this sensor, from DEVICE_CLASSES."""
|
||||||
return 'motion'
|
return "motion"
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue