This commit is contained in:
Paulus Schoutsen 2019-07-31 12:25:30 -07:00
parent da05dfe708
commit 4de97abc3a
2676 changed files with 163166 additions and 140084 deletions

View file

@ -7,9 +7,7 @@ import platform
import subprocess
import sys
import threading
from typing import ( # noqa pylint: disable=unused-import
List, Dict, Any, TYPE_CHECKING
)
from typing import List, Dict, Any, TYPE_CHECKING # noqa pylint: disable=unused-import
from homeassistant import monkey_patch
from homeassistant.const import (
@ -30,11 +28,12 @@ def set_loop() -> None:
policy = None
if sys.platform == 'win32':
if hasattr(asyncio, 'WindowsProactorEventLoopPolicy'):
if sys.platform == "win32":
if hasattr(asyncio, "WindowsProactorEventLoopPolicy"):
# pylint: disable=no-member
policy = asyncio.WindowsProactorEventLoopPolicy()
else:
class ProactorPolicy(BaseDefaultEventLoopPolicy):
"""Event loop policy to create proactor loops."""
@ -56,28 +55,40 @@ def set_loop() -> None:
def validate_python() -> None:
"""Validate that the right Python version is running."""
if sys.version_info[:3] < REQUIRED_PYTHON_VER:
print("Home Assistant requires at least Python {}.{}.{}".format(
*REQUIRED_PYTHON_VER))
print(
"Home Assistant requires at least Python {}.{}.{}".format(
*REQUIRED_PYTHON_VER
)
)
sys.exit(1)
def ensure_config_path(config_dir: str) -> None:
"""Validate the configuration directory."""
import homeassistant.config as config_util
lib_dir = os.path.join(config_dir, 'deps')
lib_dir = os.path.join(config_dir, "deps")
# Test if configuration directory exists
if not os.path.isdir(config_dir):
if config_dir != config_util.get_default_config_dir():
print(('Fatal Error: Specified configuration directory does '
'not exist {} ').format(config_dir))
print(
(
"Fatal Error: Specified configuration directory does "
"not exist {} "
).format(config_dir)
)
sys.exit(1)
try:
os.mkdir(config_dir)
except OSError:
print(('Fatal Error: Unable to create default configuration '
'directory {} ').format(config_dir))
print(
(
"Fatal Error: Unable to create default configuration "
"directory {} "
).format(config_dir)
)
sys.exit(1)
# Test if library directory exists
@ -85,20 +96,22 @@ def ensure_config_path(config_dir: str) -> None:
try:
os.mkdir(lib_dir)
except OSError:
print(('Fatal Error: Unable to create library '
'directory {} ').format(lib_dir))
print(
("Fatal Error: Unable to create library " "directory {} ").format(
lib_dir
)
)
sys.exit(1)
async def ensure_config_file(hass: 'core.HomeAssistant', config_dir: str) \
-> str:
async def ensure_config_file(hass: "core.HomeAssistant", config_dir: str) -> str:
"""Ensure configuration file exists."""
import homeassistant.config as config_util
config_path = await config_util.async_ensure_config_exists(
hass, config_dir)
config_path = await config_util.async_ensure_config_exists(hass, config_dir)
if config_path is None:
print('Error getting configuration path')
print("Error getting configuration path")
sys.exit(1)
return config_path
@ -107,71 +120,72 @@ async def ensure_config_file(hass: 'core.HomeAssistant', config_dir: str) \
def get_arguments() -> argparse.Namespace:
"""Get parsed passed in arguments."""
import homeassistant.config as config_util
parser = argparse.ArgumentParser(
description="Home Assistant: Observe, Control, Automate.")
parser.add_argument('--version', action='version', version=__version__)
description="Home Assistant: Observe, Control, Automate."
)
parser.add_argument("--version", action="version", version=__version__)
parser.add_argument(
'-c', '--config',
metavar='path_to_config_dir',
"-c",
"--config",
metavar="path_to_config_dir",
default=config_util.get_default_config_dir(),
help="Directory that contains the Home Assistant configuration")
help="Directory that contains the Home Assistant configuration",
)
parser.add_argument(
'--demo-mode',
action='store_true',
help='Start Home Assistant in demo mode')
"--demo-mode", action="store_true", help="Start Home Assistant in demo mode"
)
parser.add_argument(
'--debug',
action='store_true',
help='Start Home Assistant in debug mode')
"--debug", action="store_true", help="Start Home Assistant in debug mode"
)
parser.add_argument(
'--open-ui',
action='store_true',
help='Open the webinterface in a browser')
"--open-ui", action="store_true", help="Open the webinterface in a browser"
)
parser.add_argument(
'--skip-pip',
action='store_true',
help='Skips pip install of required packages on startup')
"--skip-pip",
action="store_true",
help="Skips pip install of required packages on startup",
)
parser.add_argument(
'-v', '--verbose',
action='store_true',
help="Enable verbose logging to file.")
"-v", "--verbose", action="store_true", help="Enable verbose logging to file."
)
parser.add_argument(
'--pid-file',
metavar='path_to_pid_file',
"--pid-file",
metavar="path_to_pid_file",
default=None,
help='Path to PID file useful for running as daemon')
help="Path to PID file useful for running as daemon",
)
parser.add_argument(
'--log-rotate-days',
"--log-rotate-days",
type=int,
default=None,
help='Enables daily log rotation and keeps up to the specified days')
help="Enables daily log rotation and keeps up to the specified days",
)
parser.add_argument(
'--log-file',
"--log-file",
type=str,
default=None,
help='Log file to write to. If not set, CONFIG/home-assistant.log '
'is used')
help="Log file to write to. If not set, CONFIG/home-assistant.log " "is used",
)
parser.add_argument(
'--log-no-color',
action='store_true',
help="Disable color logs")
"--log-no-color", action="store_true", help="Disable color logs"
)
parser.add_argument(
'--runner',
action='store_true',
help='On restart exit with code {}'.format(RESTART_EXIT_CODE))
"--runner",
action="store_true",
help="On restart exit with code {}".format(RESTART_EXIT_CODE),
)
parser.add_argument(
'--script',
nargs=argparse.REMAINDER,
help='Run one of the embedded scripts')
"--script", nargs=argparse.REMAINDER, help="Run one of the embedded scripts"
)
if os.name == "posix":
parser.add_argument(
'--daemon',
action='store_true',
help='Run Home Assistant as daemon')
"--daemon", action="store_true", help="Run Home Assistant as daemon"
)
arguments = parser.parse_args()
if os.name != "posix" or arguments.debug or arguments.runner:
setattr(arguments, 'daemon', False)
setattr(arguments, "daemon", False)
return arguments
@ -192,8 +206,8 @@ def daemonize() -> None:
sys.exit(0)
# redirect standard file descriptors to devnull
infd = open(os.devnull, 'r')
outfd = open(os.devnull, 'a+')
infd = open(os.devnull, "r")
outfd = open(os.devnull, "a+")
sys.stdout.flush()
sys.stderr.flush()
os.dup2(infd.fileno(), sys.stdin.fileno())
@ -205,7 +219,7 @@ def check_pid(pid_file: str) -> None:
"""Check that Home Assistant is not already running."""
# Check pid file
try:
with open(pid_file, 'r') as file:
with open(pid_file, "r") as file:
pid = int(file.readline())
except IOError:
# PID File does not exist
@ -220,7 +234,7 @@ def check_pid(pid_file: str) -> None:
except OSError:
# PID does not exist
return
print('Fatal Error: HomeAssistant is already running.')
print("Fatal Error: HomeAssistant is already running.")
sys.exit(1)
@ -228,10 +242,10 @@ def write_pid(pid_file: str) -> None:
"""Create a PID File."""
pid = os.getpid()
try:
with open(pid_file, 'w') as file:
with open(pid_file, "w") as file:
file.write(str(pid))
except IOError:
print('Fatal Error: Unable to write pid file {}'.format(pid_file))
print("Fatal Error: Unable to write pid file {}".format(pid_file))
sys.exit(1)
@ -255,17 +269,15 @@ def closefds_osx(min_fd: int, max_fd: int) -> None:
def cmdline() -> List[str]:
"""Collect path and arguments to re-execute the current hass instance."""
if os.path.basename(sys.argv[0]) == '__main__.py':
if os.path.basename(sys.argv[0]) == "__main__.py":
modulepath = os.path.dirname(sys.argv[0])
os.environ['PYTHONPATH'] = os.path.dirname(modulepath)
return [sys.executable] + [arg for arg in sys.argv if
arg != '--daemon']
os.environ["PYTHONPATH"] = os.path.dirname(modulepath)
return [sys.executable] + [arg for arg in sys.argv if arg != "--daemon"]
return [arg for arg in sys.argv if arg != '--daemon']
return [arg for arg in sys.argv if arg != "--daemon"]
async def setup_and_run_hass(config_dir: str,
args: argparse.Namespace) -> int:
async def setup_and_run_hass(config_dir: str, args: argparse.Namespace) -> int:
"""Set up HASS and run."""
# pylint: disable=redefined-outer-name
from homeassistant import bootstrap, core
@ -273,21 +285,29 @@ async def setup_and_run_hass(config_dir: str,
hass = core.HomeAssistant()
if args.demo_mode:
config = {
'frontend': {},
'demo': {}
} # type: Dict[str, Any]
config = {"frontend": {}, "demo": {}} # type: Dict[str, Any]
bootstrap.async_from_config_dict(
config, hass, config_dir=config_dir, verbose=args.verbose,
skip_pip=args.skip_pip, log_rotate_days=args.log_rotate_days,
log_file=args.log_file, log_no_color=args.log_no_color)
config,
hass,
config_dir=config_dir,
verbose=args.verbose,
skip_pip=args.skip_pip,
log_rotate_days=args.log_rotate_days,
log_file=args.log_file,
log_no_color=args.log_no_color,
)
else:
config_file = await ensure_config_file(hass, config_dir)
print('Config directory:', config_dir)
print("Config directory:", config_dir)
await bootstrap.async_from_config_file(
config_file, hass, verbose=args.verbose, skip_pip=args.skip_pip,
log_rotate_days=args.log_rotate_days, log_file=args.log_file,
log_no_color=args.log_no_color)
config_file,
hass,
verbose=args.verbose,
skip_pip=args.skip_pip,
log_rotate_days=args.log_rotate_days,
log_file=args.log_file,
log_no_color=args.log_no_color,
)
if args.open_ui:
# Imported here to avoid importing asyncio before monkey patch
@ -297,12 +317,14 @@ async def setup_and_run_hass(config_dir: str,
"""Open the web interface in a browser."""
if hass.config.api is not None:
import webbrowser
webbrowser.open(hass.config.api.base_url)
run_callback_threadsafe(
hass.loop,
hass.bus.async_listen_once,
EVENT_HOMEASSISTANT_START, open_browser
EVENT_HOMEASSISTANT_START,
open_browser,
)
return await hass.async_run()
@ -312,17 +334,17 @@ def try_to_restart() -> None:
"""Attempt to clean up state and start a new Home Assistant instance."""
# Things should be mostly shut down already at this point, now just try
# to clean up things that may have been left behind.
sys.stderr.write('Home Assistant attempting to restart.\n')
sys.stderr.write("Home Assistant attempting to restart.\n")
# Count remaining threads, ideally there should only be one non-daemonized
# thread left (which is us). Nothing we really do with it, but it might be
# useful when debugging shutdown/restart issues.
try:
nthreads = sum(thread.is_alive() and not thread.daemon
for thread in threading.enumerate())
nthreads = sum(
thread.is_alive() and not thread.daemon for thread in threading.enumerate()
)
if nthreads > 1:
sys.stderr.write(
"Found {} non-daemonic threads.\n".format(nthreads))
sys.stderr.write("Found {} non-daemonic threads.\n".format(nthreads))
# Somehow we sometimes seem to trigger an assertion in the python threading
# module. It seems we find threads that have no associated OS level thread
@ -336,7 +358,7 @@ def try_to_restart() -> None:
except ValueError:
max_fd = 256
if platform.system() == 'Darwin':
if platform.system() == "Darwin":
closefds_osx(3, max_fd)
else:
os.closerange(3, max_fd)
@ -355,15 +377,15 @@ def main() -> int:
validate_python()
monkey_patch_needed = sys.version_info[:3] < (3, 6, 3)
if monkey_patch_needed and os.environ.get('HASS_NO_MONKEY') != '1':
if monkey_patch_needed and os.environ.get("HASS_NO_MONKEY") != "1":
monkey_patch.disable_c_asyncio()
monkey_patch.patch_weakref_tasks()
set_loop()
# Run a simple daemon runner process on Windows to handle restarts
if os.name == 'nt' and '--runner' not in sys.argv:
nt_args = cmdline() + ['--runner']
if os.name == "nt" and "--runner" not in sys.argv:
nt_args = cmdline() + ["--runner"]
while True:
try:
subprocess.check_call(nt_args)
@ -378,6 +400,7 @@ def main() -> int:
if args.script is not None:
from homeassistant import scripts
return scripts.run(args.script)
config_dir = os.path.join(os.getcwd(), args.config)
@ -392,6 +415,7 @@ def main() -> int:
write_pid(args.pid_file)
from homeassistant.util.async_ import asyncio_run
exit_code = asyncio_run(setup_and_run_hass(config_dir, args))
if exit_code == RESTART_EXIT_CODE and not args.runner:
try_to_restart()

View file

@ -17,8 +17,8 @@ from .const import GROUP_ID_ADMIN
from .mfa_modules import auth_mfa_module_from_config, MultiFactorAuthModule
from .providers import auth_provider_from_config, AuthProvider, LoginFlow
EVENT_USER_ADDED = 'user_added'
EVENT_USER_REMOVED = 'user_removed'
EVENT_USER_ADDED = "user_added"
EVENT_USER_REMOVED = "user_removed"
_LOGGER = logging.getLogger(__name__)
_MfaModuleDict = Dict[str, MultiFactorAuthModule]
@ -27,9 +27,10 @@ _ProviderDict = Dict[_ProviderKey, AuthProvider]
async def auth_manager_from_config(
hass: HomeAssistant,
provider_configs: List[Dict[str, Any]],
module_configs: List[Dict[str, Any]]) -> 'AuthManager':
hass: HomeAssistant,
provider_configs: List[Dict[str, Any]],
module_configs: List[Dict[str, Any]],
) -> "AuthManager":
"""Initialize an auth manager from config.
CORE_CONFIG_SCHEMA will make sure do duplicated auth providers or
@ -38,8 +39,11 @@ async def auth_manager_from_config(
store = auth_store.AuthStore(hass)
if provider_configs:
providers = await asyncio.gather(
*(auth_provider_from_config(hass, store, config)
for config in provider_configs))
*(
auth_provider_from_config(hass, store, config)
for config in provider_configs
)
)
else:
providers = ()
# So returned auth providers are in same order as config
@ -50,8 +54,8 @@ async def auth_manager_from_config(
if module_configs:
modules = await asyncio.gather(
*(auth_mfa_module_from_config(hass, config)
for config in module_configs))
*(auth_mfa_module_from_config(hass, config) for config in module_configs)
)
else:
modules = ()
# So returned auth modules are in same order as config
@ -66,17 +70,21 @@ async def auth_manager_from_config(
class AuthManager:
"""Manage the authentication for Home Assistant."""
def __init__(self, hass: HomeAssistant, store: auth_store.AuthStore,
providers: _ProviderDict, mfa_modules: _MfaModuleDict) \
-> None:
def __init__(
self,
hass: HomeAssistant,
store: auth_store.AuthStore,
providers: _ProviderDict,
mfa_modules: _MfaModuleDict,
) -> None:
"""Initialize the auth manager."""
self.hass = hass
self._store = store
self._providers = providers
self._mfa_modules = mfa_modules
self.login_flow = data_entry_flow.FlowManager(
hass, self._async_create_login_flow,
self._async_finish_login_flow)
hass, self._async_create_login_flow, self._async_finish_login_flow
)
@property
def support_legacy(self) -> bool:
@ -86,7 +94,7 @@ class AuthManager:
Should be removed when we removed legacy_api_password auth providers.
"""
for provider_type, _ in self._providers:
if provider_type == 'legacy_api_password':
if provider_type == "legacy_api_password":
return True
return False
@ -100,20 +108,21 @@ class AuthManager:
"""Return a list of available auth modules."""
return list(self._mfa_modules.values())
def get_auth_provider(self, provider_type: str, provider_id: str) \
-> Optional[AuthProvider]:
def get_auth_provider(
self, provider_type: str, provider_id: str
) -> Optional[AuthProvider]:
"""Return an auth provider, None if not found."""
return self._providers.get((provider_type, provider_id))
def get_auth_providers(self, provider_type: str) \
-> List[AuthProvider]:
def get_auth_providers(self, provider_type: str) -> List[AuthProvider]:
"""Return a List of auth provider of one type, Empty if not found."""
return [provider
for (p_type, _), provider in self._providers.items()
if p_type == provider_type]
return [
provider
for (p_type, _), provider in self._providers.items()
if p_type == provider_type
]
def get_auth_mfa_module(self, module_id: str) \
-> Optional[MultiFactorAuthModule]:
def get_auth_mfa_module(self, module_id: str) -> Optional[MultiFactorAuthModule]:
"""Return a multi-factor auth module, None if not found."""
return self._mfa_modules.get(module_id)
@ -135,7 +144,8 @@ class AuthManager:
return await self._store.async_get_group(group_id)
async def async_get_user_by_credentials(
self, credentials: models.Credentials) -> Optional[models.User]:
self, credentials: models.Credentials
) -> Optional[models.User]:
"""Get a user by credential, return None if not found."""
for user in await self.async_get_users():
for creds in user.credentials:
@ -145,57 +155,50 @@ class AuthManager:
return None
async def async_create_system_user(
self, name: str,
group_ids: Optional[List[str]] = None) -> models.User:
self, name: str, group_ids: Optional[List[str]] = None
) -> models.User:
"""Create a system user."""
user = await self._store.async_create_user(
name=name,
system_generated=True,
is_active=True,
group_ids=group_ids or [],
name=name, system_generated=True, is_active=True, group_ids=group_ids or []
)
self.hass.bus.async_fire(EVENT_USER_ADDED, {
'user_id': user.id
})
self.hass.bus.async_fire(EVENT_USER_ADDED, {"user_id": user.id})
return user
async def async_create_user(self, name: str) -> models.User:
"""Create a user."""
kwargs = {
'name': name,
'is_active': True,
'group_ids': [GROUP_ID_ADMIN]
"name": name,
"is_active": True,
"group_ids": [GROUP_ID_ADMIN],
} # type: Dict[str, Any]
if await self._user_should_be_owner():
kwargs['is_owner'] = True
kwargs["is_owner"] = True
user = await self._store.async_create_user(**kwargs)
self.hass.bus.async_fire(EVENT_USER_ADDED, {
'user_id': user.id
})
self.hass.bus.async_fire(EVENT_USER_ADDED, {"user_id": user.id})
return user
async def async_get_or_create_user(self, credentials: models.Credentials) \
-> models.User:
async def async_get_or_create_user(
self, credentials: models.Credentials
) -> models.User:
"""Get or create a user."""
if not credentials.is_new:
user = await self.async_get_user_by_credentials(credentials)
if user is None:
raise ValueError('Unable to find the user.')
raise ValueError("Unable to find the user.")
return user
auth_provider = self._async_get_auth_provider(credentials)
if auth_provider is None:
raise RuntimeError('Credential with unknown provider encountered')
raise RuntimeError("Credential with unknown provider encountered")
info = await auth_provider.async_user_meta_for_credentials(
credentials)
info = await auth_provider.async_user_meta_for_credentials(credentials)
user = await self._store.async_create_user(
credentials=credentials,
@ -204,14 +207,13 @@ class AuthManager:
group_ids=[GROUP_ID_ADMIN],
)
self.hass.bus.async_fire(EVENT_USER_ADDED, {
'user_id': user.id
})
self.hass.bus.async_fire(EVENT_USER_ADDED, {"user_id": user.id})
return user
async def async_link_user(self, user: models.User,
credentials: models.Credentials) -> None:
async def async_link_user(
self, user: models.User, credentials: models.Credentials
) -> None:
"""Link credentials to an existing user."""
await self._store.async_link_user(user, credentials)
@ -227,19 +229,20 @@ class AuthManager:
await self._store.async_remove_user(user)
self.hass.bus.async_fire(EVENT_USER_REMOVED, {
'user_id': user.id
})
self.hass.bus.async_fire(EVENT_USER_REMOVED, {"user_id": user.id})
async def async_update_user(self, user: models.User,
name: Optional[str] = None,
group_ids: Optional[List[str]] = None) -> None:
async def async_update_user(
self,
user: models.User,
name: Optional[str] = None,
group_ids: Optional[List[str]] = None,
) -> None:
"""Update a user."""
kwargs = {} # type: Dict[str,Any]
if name is not None:
kwargs['name'] = name
kwargs["name"] = name
if group_ids is not None:
kwargs['group_ids'] = group_ids
kwargs["group_ids"] = group_ids
await self._store.async_update_user(user, **kwargs)
async def async_activate_user(self, user: models.User) -> None:
@ -249,47 +252,52 @@ class AuthManager:
async def async_deactivate_user(self, user: models.User) -> None:
"""Deactivate a user."""
if user.is_owner:
raise ValueError('Unable to deactive the owner')
raise ValueError("Unable to deactive the owner")
await self._store.async_deactivate_user(user)
async def async_remove_credentials(
self, credentials: models.Credentials) -> None:
async def async_remove_credentials(self, credentials: models.Credentials) -> None:
"""Remove credentials."""
provider = self._async_get_auth_provider(credentials)
if (provider is not None and
hasattr(provider, 'async_will_remove_credentials')):
if provider is not None and hasattr(provider, "async_will_remove_credentials"):
# https://github.com/python/mypy/issues/1424
await provider.async_will_remove_credentials( # type: ignore
credentials)
credentials
)
await self._store.async_remove_credentials(credentials)
async def async_enable_user_mfa(self, user: models.User,
mfa_module_id: str, data: Any) -> None:
async def async_enable_user_mfa(
self, user: models.User, mfa_module_id: str, data: Any
) -> None:
"""Enable a multi-factor auth module for user."""
if user.system_generated:
raise ValueError('System generated users cannot enable '
'multi-factor auth module.')
raise ValueError(
"System generated users cannot enable " "multi-factor auth module."
)
module = self.get_auth_mfa_module(mfa_module_id)
if module is None:
raise ValueError('Unable find multi-factor auth module: {}'
.format(mfa_module_id))
raise ValueError(
"Unable find multi-factor auth module: {}".format(mfa_module_id)
)
await module.async_setup_user(user.id, data)
async def async_disable_user_mfa(self, user: models.User,
mfa_module_id: str) -> None:
async def async_disable_user_mfa(
self, user: models.User, mfa_module_id: str
) -> None:
"""Disable a multi-factor auth module for user."""
if user.system_generated:
raise ValueError('System generated users cannot disable '
'multi-factor auth module.')
raise ValueError(
"System generated users cannot disable " "multi-factor auth module."
)
module = self.get_auth_mfa_module(mfa_module_id)
if module is None:
raise ValueError('Unable find multi-factor auth module: {}'
.format(mfa_module_id))
raise ValueError(
"Unable find multi-factor auth module: {}".format(mfa_module_id)
)
await module.async_depose_user(user.id)
@ -302,20 +310,23 @@ class AuthManager:
return modules
async def async_create_refresh_token(
self, user: models.User, client_id: Optional[str] = None,
client_name: Optional[str] = None,
client_icon: Optional[str] = None,
token_type: Optional[str] = None,
access_token_expiration: timedelta = ACCESS_TOKEN_EXPIRATION) \
-> models.RefreshToken:
self,
user: models.User,
client_id: Optional[str] = None,
client_name: Optional[str] = None,
client_icon: Optional[str] = None,
token_type: Optional[str] = None,
access_token_expiration: timedelta = ACCESS_TOKEN_EXPIRATION,
) -> models.RefreshToken:
"""Create a new refresh token for a user."""
if not user.is_active:
raise ValueError('User is not active')
raise ValueError("User is not active")
if user.system_generated and client_id is not None:
raise ValueError(
'System generated users cannot have refresh tokens connected '
'to a client.')
"System generated users cannot have refresh tokens connected "
"to a client."
)
if token_type is None:
if user.system_generated:
@ -325,61 +336,76 @@ class AuthManager:
if user.system_generated != (token_type == models.TOKEN_TYPE_SYSTEM):
raise ValueError(
'System generated users can only have system type '
'refresh tokens')
"System generated users can only have system type " "refresh tokens"
)
if token_type == models.TOKEN_TYPE_NORMAL and client_id is None:
raise ValueError('Client is required to generate a refresh token.')
raise ValueError("Client is required to generate a refresh token.")
if (token_type == models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN and
client_name is None):
raise ValueError('Client_name is required for long-lived access '
'token')
if (
token_type == models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN
and client_name is None
):
raise ValueError("Client_name is required for long-lived access " "token")
if token_type == models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN:
for token in user.refresh_tokens.values():
if (token.client_name == client_name and token.token_type ==
models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN):
if (
token.client_name == client_name
and token.token_type == models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN
):
# Each client_name can only have one
# long_lived_access_token type of refresh token
raise ValueError('{} already exists'.format(client_name))
raise ValueError("{} already exists".format(client_name))
return await self._store.async_create_refresh_token(
user, client_id, client_name, client_icon,
token_type, access_token_expiration)
user,
client_id,
client_name,
client_icon,
token_type,
access_token_expiration,
)
async def async_get_refresh_token(
self, token_id: str) -> Optional[models.RefreshToken]:
self, token_id: str
) -> Optional[models.RefreshToken]:
"""Get refresh token by id."""
return await self._store.async_get_refresh_token(token_id)
async def async_get_refresh_token_by_token(
self, token: str) -> Optional[models.RefreshToken]:
self, token: str
) -> Optional[models.RefreshToken]:
"""Get refresh token by token."""
return await self._store.async_get_refresh_token_by_token(token)
async def async_remove_refresh_token(self,
refresh_token: models.RefreshToken) \
-> None:
async def async_remove_refresh_token(
self, refresh_token: models.RefreshToken
) -> None:
"""Delete a refresh token."""
await self._store.async_remove_refresh_token(refresh_token)
@callback
def async_create_access_token(self,
refresh_token: models.RefreshToken,
remote_ip: Optional[str] = None) -> str:
def async_create_access_token(
self, refresh_token: models.RefreshToken, remote_ip: Optional[str] = None
) -> str:
"""Create a new access token."""
self._store.async_log_refresh_token_usage(refresh_token, remote_ip)
now = dt_util.utcnow()
return jwt.encode({
'iss': refresh_token.id,
'iat': now,
'exp': now + refresh_token.access_token_expiration,
}, refresh_token.jwt_key, algorithm='HS256').decode()
return jwt.encode(
{
"iss": refresh_token.id,
"iat": now,
"exp": now + refresh_token.access_token_expiration,
},
refresh_token.jwt_key,
algorithm="HS256",
).decode()
async def async_validate_access_token(
self, token: str) -> Optional[models.RefreshToken]:
self, token: str
) -> Optional[models.RefreshToken]:
"""Return refresh token if an access token is valid."""
try:
unverif_claims = jwt.decode(token, verify=False)
@ -387,23 +413,18 @@ class AuthManager:
return None
refresh_token = await self.async_get_refresh_token(
cast(str, unverif_claims.get('iss')))
cast(str, unverif_claims.get("iss"))
)
if refresh_token is None:
jwt_key = ''
issuer = ''
jwt_key = ""
issuer = ""
else:
jwt_key = refresh_token.jwt_key
issuer = refresh_token.id
try:
jwt.decode(
token,
jwt_key,
leeway=10,
issuer=issuer,
algorithms=['HS256']
)
jwt.decode(token, jwt_key, leeway=10, issuer=issuer, algorithms=["HS256"])
except jwt.InvalidTokenError:
return None
@ -413,31 +434,32 @@ class AuthManager:
return refresh_token
async def _async_create_login_flow(
self, handler: _ProviderKey, *, context: Optional[Dict],
data: Optional[Any]) -> data_entry_flow.FlowHandler:
self, handler: _ProviderKey, *, context: Optional[Dict], data: Optional[Any]
) -> data_entry_flow.FlowHandler:
"""Create a login flow."""
auth_provider = self._providers[handler]
return await auth_provider.async_login_flow(context)
async def _async_finish_login_flow(
self, flow: LoginFlow, result: Dict[str, Any]) \
-> Dict[str, Any]:
self, flow: LoginFlow, result: Dict[str, Any]
) -> Dict[str, Any]:
"""Return a user as result of login flow."""
if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
if result["type"] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
return result
# we got final result
if isinstance(result['data'], models.User):
result['result'] = result['data']
if isinstance(result["data"], models.User):
result["result"] = result["data"]
return result
auth_provider = self._providers[result['handler']]
auth_provider = self._providers[result["handler"]]
credentials = await auth_provider.async_get_or_create_credentials(
result['data'])
result["data"]
)
if flow.context is not None and flow.context.get('credential_only'):
result['result'] = credentials
if flow.context is not None and flow.context.get("credential_only"):
result["result"] = credentials
return result
# multi-factor module cannot enabled for new credential
@ -452,15 +474,18 @@ class AuthManager:
flow.available_mfa_modules = modules
return await flow.async_step_select_mfa_module()
result['result'] = await self.async_get_or_create_user(credentials)
result["result"] = await self.async_get_or_create_user(credentials)
return result
@callback
def _async_get_auth_provider(
self, credentials: models.Credentials) -> Optional[AuthProvider]:
self, credentials: models.Credentials
) -> Optional[AuthProvider]:
"""Get auth provider from a set of credentials."""
auth_provider_key = (credentials.auth_provider_type,
credentials.auth_provider_id)
auth_provider_key = (
credentials.auth_provider_type,
credentials.auth_provider_id,
)
return self._providers.get(auth_provider_key)
async def _user_should_be_owner(self) -> bool:

View file

@ -16,10 +16,10 @@ from .permissions import PermissionLookup, system_policies
from .permissions.types import PolicyType # noqa: F401
STORAGE_VERSION = 1
STORAGE_KEY = 'auth'
GROUP_NAME_ADMIN = 'Administrators'
STORAGE_KEY = "auth"
GROUP_NAME_ADMIN = "Administrators"
GROUP_NAME_USER = "Users"
GROUP_NAME_READ_ONLY = 'Read Only'
GROUP_NAME_READ_ONLY = "Read Only"
class AuthStore:
@ -37,8 +37,9 @@ class AuthStore:
self._users = None # type: Optional[Dict[str, models.User]]
self._groups = None # type: Optional[Dict[str, models.Group]]
self._perm_lookup = None # type: Optional[PermissionLookup]
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY,
private=True)
self._store = hass.helpers.storage.Store(
STORAGE_VERSION, STORAGE_KEY, private=True
)
self._lock = asyncio.Lock()
async def async_get_groups(self) -> List[models.Group]:
@ -74,11 +75,14 @@ class AuthStore:
return self._users.get(user_id)
async def async_create_user(
self, name: Optional[str], is_owner: Optional[bool] = None,
is_active: Optional[bool] = None,
system_generated: Optional[bool] = None,
credentials: Optional[models.Credentials] = None,
group_ids: Optional[List[str]] = None) -> models.User:
self,
name: Optional[str],
is_owner: Optional[bool] = None,
is_active: Optional[bool] = None,
system_generated: Optional[bool] = None,
credentials: Optional[models.Credentials] = None,
group_ids: Optional[List[str]] = None,
) -> models.User:
"""Create a new user."""
if self._users is None:
await self._async_load()
@ -87,28 +91,28 @@ class AuthStore:
assert self._groups is not None
groups = []
for group_id in (group_ids or []):
for group_id in group_ids or []:
group = self._groups.get(group_id)
if group is None:
raise ValueError('Invalid group specified {}'.format(group_id))
raise ValueError("Invalid group specified {}".format(group_id))
groups.append(group)
kwargs = {
'name': name,
"name": name,
# Until we get group management, we just put everyone in the
# same group.
'groups': groups,
'perm_lookup': self._perm_lookup,
"groups": groups,
"perm_lookup": self._perm_lookup,
} # type: Dict[str, Any]
if is_owner is not None:
kwargs['is_owner'] = is_owner
kwargs["is_owner"] = is_owner
if is_active is not None:
kwargs['is_active'] = is_active
kwargs["is_active"] = is_active
if system_generated is not None:
kwargs['system_generated'] = system_generated
kwargs["system_generated"] = system_generated
new_user = models.User(**kwargs)
@ -122,8 +126,9 @@ class AuthStore:
await self.async_link_user(new_user, credentials)
return new_user
async def async_link_user(self, user: models.User,
credentials: models.Credentials) -> None:
async def async_link_user(
self, user: models.User, credentials: models.Credentials
) -> None:
"""Add credentials to an existing user."""
user.credentials.append(credentials)
self._async_schedule_save()
@ -139,9 +144,12 @@ class AuthStore:
self._async_schedule_save()
async def async_update_user(
self, user: models.User, name: Optional[str] = None,
is_active: Optional[bool] = None,
group_ids: Optional[List[str]] = None) -> None:
self,
user: models.User,
name: Optional[str] = None,
is_active: Optional[bool] = None,
group_ids: Optional[List[str]] = None,
) -> None:
"""Update a user."""
assert self._groups is not None
@ -156,10 +164,7 @@ class AuthStore:
user.groups = groups
user.invalidate_permission_cache()
for attr_name, value in (
('name', name),
('is_active', is_active),
):
for attr_name, value in (("name", name), ("is_active", is_active)):
if value is not None:
setattr(user, attr_name, value)
@ -175,8 +180,7 @@ class AuthStore:
user.is_active = False
self._async_schedule_save()
async def async_remove_credentials(
self, credentials: models.Credentials) -> None:
async def async_remove_credentials(self, credentials: models.Credentials) -> None:
"""Remove credentials."""
if self._users is None:
await self._async_load()
@ -197,23 +201,25 @@ class AuthStore:
self._async_schedule_save()
async def async_create_refresh_token(
self, user: models.User, client_id: Optional[str] = None,
client_name: Optional[str] = None,
client_icon: Optional[str] = None,
token_type: str = models.TOKEN_TYPE_NORMAL,
access_token_expiration: timedelta = ACCESS_TOKEN_EXPIRATION) \
-> models.RefreshToken:
self,
user: models.User,
client_id: Optional[str] = None,
client_name: Optional[str] = None,
client_icon: Optional[str] = None,
token_type: str = models.TOKEN_TYPE_NORMAL,
access_token_expiration: timedelta = ACCESS_TOKEN_EXPIRATION,
) -> models.RefreshToken:
"""Create a new token for a user."""
kwargs = {
'user': user,
'client_id': client_id,
'token_type': token_type,
'access_token_expiration': access_token_expiration
"user": user,
"client_id": client_id,
"token_type": token_type,
"access_token_expiration": access_token_expiration,
} # type: Dict[str, Any]
if client_name:
kwargs['client_name'] = client_name
kwargs["client_name"] = client_name
if client_icon:
kwargs['client_icon'] = client_icon
kwargs["client_icon"] = client_icon
refresh_token = models.RefreshToken(**kwargs)
user.refresh_tokens[refresh_token.id] = refresh_token
@ -222,7 +228,8 @@ class AuthStore:
return refresh_token
async def async_remove_refresh_token(
self, refresh_token: models.RefreshToken) -> None:
self, refresh_token: models.RefreshToken
) -> None:
"""Remove a refresh token."""
if self._users is None:
await self._async_load()
@ -234,7 +241,8 @@ class AuthStore:
break
async def async_get_refresh_token(
self, token_id: str) -> Optional[models.RefreshToken]:
self, token_id: str
) -> Optional[models.RefreshToken]:
"""Get refresh token by id."""
if self._users is None:
await self._async_load()
@ -248,7 +256,8 @@ class AuthStore:
return None
async def async_get_refresh_token_by_token(
self, token: str) -> Optional[models.RefreshToken]:
self, token: str
) -> Optional[models.RefreshToken]:
"""Get refresh token by token."""
if self._users is None:
await self._async_load()
@ -265,8 +274,8 @@ class AuthStore:
@callback
def async_log_refresh_token_usage(
self, refresh_token: models.RefreshToken,
remote_ip: Optional[str] = None) -> None:
self, refresh_token: models.RefreshToken, remote_ip: Optional[str] = None
) -> None:
"""Update refresh token last used information."""
refresh_token.last_used_at = dt_util.utcnow()
refresh_token.last_used_ip = remote_ip
@ -292,9 +301,7 @@ class AuthStore:
if self._users is not None:
return
self._perm_lookup = perm_lookup = PermissionLookup(
ent_reg, dev_reg
)
self._perm_lookup = perm_lookup = PermissionLookup(ent_reg, dev_reg)
if data is None:
self._set_defaults()
@ -317,24 +324,24 @@ class AuthStore:
# prevents crashing if user rolls back HA version after a new property
# was added.
for group_dict in data.get('groups', []):
for group_dict in data.get("groups", []):
policy = None # type: Optional[PolicyType]
if group_dict['id'] == GROUP_ID_ADMIN:
if group_dict["id"] == GROUP_ID_ADMIN:
has_admin_group = True
name = GROUP_NAME_ADMIN
policy = system_policies.ADMIN_POLICY
system_generated = True
elif group_dict['id'] == GROUP_ID_USER:
elif group_dict["id"] == GROUP_ID_USER:
has_user_group = True
name = GROUP_NAME_USER
policy = system_policies.USER_POLICY
system_generated = True
elif group_dict['id'] == GROUP_ID_READ_ONLY:
elif group_dict["id"] == GROUP_ID_READ_ONLY:
has_read_only_group = True
name = GROUP_NAME_READ_ONLY
@ -342,18 +349,18 @@ class AuthStore:
system_generated = True
else:
name = group_dict['name']
policy = group_dict.get('policy')
name = group_dict["name"]
policy = group_dict.get("policy")
system_generated = False
# We don't want groups without a policy that are not system groups
# This is part of migrating from state 1
if policy is None:
group_without_policy = group_dict['id']
group_without_policy = group_dict["id"]
continue
groups[group_dict['id']] = models.Group(
id=group_dict['id'],
groups[group_dict["id"]] = models.Group(
id=group_dict["id"],
name=name,
policy=policy,
system_generated=system_generated,
@ -361,8 +368,7 @@ class AuthStore:
# If there are no groups, add all existing users to the admin group.
# This is part of migrating from state 2
migrate_users_to_admin_group = (not groups and
group_without_policy is None)
migrate_users_to_admin_group = not groups and group_without_policy is None
# If we find a no_policy_group, we need to migrate all users to the
# admin group. We only do this if there are no other groups, as is
@ -385,82 +391,86 @@ class AuthStore:
user_group = _system_user_group()
groups[user_group.id] = user_group
for user_dict in data['users']:
for user_dict in data["users"]:
# Collect the users group.
user_groups = []
for group_id in user_dict.get('group_ids', []):
for group_id in user_dict.get("group_ids", []):
# This is part of migrating from state 1
if group_id == group_without_policy:
group_id = GROUP_ID_ADMIN
user_groups.append(groups[group_id])
# This is part of migrating from state 2
if (not user_dict['system_generated'] and
migrate_users_to_admin_group):
if not user_dict["system_generated"] and migrate_users_to_admin_group:
user_groups.append(groups[GROUP_ID_ADMIN])
users[user_dict['id']] = models.User(
name=user_dict['name'],
users[user_dict["id"]] = models.User(
name=user_dict["name"],
groups=user_groups,
id=user_dict['id'],
is_owner=user_dict['is_owner'],
is_active=user_dict['is_active'],
system_generated=user_dict['system_generated'],
id=user_dict["id"],
is_owner=user_dict["is_owner"],
is_active=user_dict["is_active"],
system_generated=user_dict["system_generated"],
perm_lookup=perm_lookup,
)
for cred_dict in data['credentials']:
users[cred_dict['user_id']].credentials.append(models.Credentials(
id=cred_dict['id'],
is_new=False,
auth_provider_type=cred_dict['auth_provider_type'],
auth_provider_id=cred_dict['auth_provider_id'],
data=cred_dict['data'],
))
for cred_dict in data["credentials"]:
users[cred_dict["user_id"]].credentials.append(
models.Credentials(
id=cred_dict["id"],
is_new=False,
auth_provider_type=cred_dict["auth_provider_type"],
auth_provider_id=cred_dict["auth_provider_id"],
data=cred_dict["data"],
)
)
for rt_dict in data['refresh_tokens']:
for rt_dict in data["refresh_tokens"]:
# Filter out the old keys that don't have jwt_key (pre-0.76)
if 'jwt_key' not in rt_dict:
if "jwt_key" not in rt_dict:
continue
created_at = dt_util.parse_datetime(rt_dict['created_at'])
created_at = dt_util.parse_datetime(rt_dict["created_at"])
if created_at is None:
getLogger(__name__).error(
'Ignoring refresh token %(id)s with invalid created_at '
'%(created_at)s for user_id %(user_id)s', rt_dict)
"Ignoring refresh token %(id)s with invalid created_at "
"%(created_at)s for user_id %(user_id)s",
rt_dict,
)
continue
token_type = rt_dict.get('token_type')
token_type = rt_dict.get("token_type")
if token_type is None:
if rt_dict['client_id'] is None:
if rt_dict["client_id"] is None:
token_type = models.TOKEN_TYPE_SYSTEM
else:
token_type = models.TOKEN_TYPE_NORMAL
# old refresh_token don't have last_used_at (pre-0.78)
last_used_at_str = rt_dict.get('last_used_at')
last_used_at_str = rt_dict.get("last_used_at")
if last_used_at_str:
last_used_at = dt_util.parse_datetime(last_used_at_str)
else:
last_used_at = None
token = models.RefreshToken(
id=rt_dict['id'],
user=users[rt_dict['user_id']],
client_id=rt_dict['client_id'],
id=rt_dict["id"],
user=users[rt_dict["user_id"]],
client_id=rt_dict["client_id"],
# use dict.get to keep backward compatibility
client_name=rt_dict.get('client_name'),
client_icon=rt_dict.get('client_icon'),
client_name=rt_dict.get("client_name"),
client_icon=rt_dict.get("client_icon"),
token_type=token_type,
created_at=created_at,
access_token_expiration=timedelta(
seconds=rt_dict['access_token_expiration']),
token=rt_dict['token'],
jwt_key=rt_dict['jwt_key'],
seconds=rt_dict["access_token_expiration"]
),
token=rt_dict["token"],
jwt_key=rt_dict["jwt_key"],
last_used_at=last_used_at,
last_used_ip=rt_dict.get('last_used_ip'),
last_used_ip=rt_dict.get("last_used_ip"),
)
users[rt_dict['user_id']].refresh_tokens[token.id] = token
users[rt_dict["user_id"]].refresh_tokens[token.id] = token
self._groups = groups
self._users = users
@ -481,12 +491,12 @@ class AuthStore:
users = [
{
'id': user.id,
'group_ids': [group.id for group in user.groups],
'is_owner': user.is_owner,
'is_active': user.is_active,
'name': user.name,
'system_generated': user.system_generated,
"id": user.id,
"group_ids": [group.id for group in user.groups],
"is_owner": user.is_owner,
"is_active": user.is_active,
"name": user.name,
"system_generated": user.system_generated,
}
for user in self._users.values()
]
@ -494,23 +504,23 @@ class AuthStore:
groups = []
for group in self._groups.values():
g_dict = {
'id': group.id,
"id": group.id,
# Name not read for sys groups. Kept here for backwards compat
'name': group.name
"name": group.name,
} # type: Dict[str, Any]
if not group.system_generated:
g_dict['policy'] = group.policy
g_dict["policy"] = group.policy
groups.append(g_dict)
credentials = [
{
'id': credential.id,
'user_id': user.id,
'auth_provider_type': credential.auth_provider_type,
'auth_provider_id': credential.auth_provider_id,
'data': credential.data,
"id": credential.id,
"user_id": user.id,
"auth_provider_type": credential.auth_provider_type,
"auth_provider_id": credential.auth_provider_id,
"data": credential.data,
}
for user in self._users.values()
for credential in user.credentials
@ -518,31 +528,30 @@ class AuthStore:
refresh_tokens = [
{
'id': refresh_token.id,
'user_id': user.id,
'client_id': refresh_token.client_id,
'client_name': refresh_token.client_name,
'client_icon': refresh_token.client_icon,
'token_type': refresh_token.token_type,
'created_at': refresh_token.created_at.isoformat(),
'access_token_expiration':
refresh_token.access_token_expiration.total_seconds(),
'token': refresh_token.token,
'jwt_key': refresh_token.jwt_key,
'last_used_at':
refresh_token.last_used_at.isoformat()
if refresh_token.last_used_at else None,
'last_used_ip': refresh_token.last_used_ip,
"id": refresh_token.id,
"user_id": user.id,
"client_id": refresh_token.client_id,
"client_name": refresh_token.client_name,
"client_icon": refresh_token.client_icon,
"token_type": refresh_token.token_type,
"created_at": refresh_token.created_at.isoformat(),
"access_token_expiration": refresh_token.access_token_expiration.total_seconds(),
"token": refresh_token.token,
"jwt_key": refresh_token.jwt_key,
"last_used_at": refresh_token.last_used_at.isoformat()
if refresh_token.last_used_at
else None,
"last_used_ip": refresh_token.last_used_ip,
}
for user in self._users.values()
for refresh_token in user.refresh_tokens.values()
]
return {
'users': users,
'groups': groups,
'credentials': credentials,
'refresh_tokens': refresh_tokens,
"users": users,
"groups": groups,
"credentials": credentials,
"refresh_tokens": refresh_tokens,
}
def _set_defaults(self) -> None:

View file

@ -4,6 +4,6 @@ from datetime import timedelta
ACCESS_TOKEN_EXPIRATION = timedelta(minutes=30)
MFA_SESSION_EXPIRATION = timedelta(minutes=5)
GROUP_ID_ADMIN = 'system-admin'
GROUP_ID_USER = 'system-users'
GROUP_ID_READ_ONLY = 'system-read-only'
GROUP_ID_ADMIN = "system-admin"
GROUP_ID_USER = "system-users"
GROUP_ID_READ_ONLY = "system-read-only"

View file

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

View file

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

View file

@ -15,26 +15,32 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ServiceNotFound
from homeassistant.helpers import config_validation as cv
from . import MultiFactorAuthModule, MULTI_FACTOR_AUTH_MODULES, \
MULTI_FACTOR_AUTH_MODULE_SCHEMA, SetupFlow
from . import (
MultiFactorAuthModule,
MULTI_FACTOR_AUTH_MODULES,
MULTI_FACTOR_AUTH_MODULE_SCHEMA,
SetupFlow,
)
REQUIREMENTS = ['pyotp==2.2.7']
REQUIREMENTS = ["pyotp==2.2.7"]
CONF_MESSAGE = 'message'
CONF_MESSAGE = "message"
CONFIG_SCHEMA = MULTI_FACTOR_AUTH_MODULE_SCHEMA.extend({
vol.Optional(CONF_INCLUDE): vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_EXCLUDE): vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_MESSAGE,
default='{} is your Home Assistant login code'): str
}, extra=vol.PREVENT_EXTRA)
CONFIG_SCHEMA = MULTI_FACTOR_AUTH_MODULE_SCHEMA.extend(
{
vol.Optional(CONF_INCLUDE): vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_EXCLUDE): vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_MESSAGE, default="{} is your Home Assistant login code"): str,
},
extra=vol.PREVENT_EXTRA,
)
STORAGE_VERSION = 1
STORAGE_KEY = 'auth_module.notify'
STORAGE_USERS = 'users'
STORAGE_USER_ID = 'user_id'
STORAGE_KEY = "auth_module.notify"
STORAGE_USERS = "users"
STORAGE_USER_ID = "user_id"
INPUT_FIELD_CODE = 'code'
INPUT_FIELD_CODE = "code"
_LOGGER = logging.getLogger(__name__)
@ -42,24 +48,28 @@ _LOGGER = logging.getLogger(__name__)
def _generate_secret() -> str:
"""Generate a secret."""
import pyotp
return str(pyotp.random_base32())
def _generate_random() -> int:
"""Generate a 8 digit number."""
import pyotp
return int(pyotp.random_base32(length=8, chars=list('1234567890')))
return int(pyotp.random_base32(length=8, chars=list("1234567890")))
def _generate_otp(secret: str, count: int) -> str:
"""Generate one time password."""
import pyotp
return str(pyotp.HOTP(secret).at(count))
def _verify_otp(secret: str, otp: str, count: int) -> bool:
"""Verify one time password."""
import pyotp
return bool(pyotp.HOTP(secret).verify(otp, count))
@ -67,7 +77,7 @@ def _verify_otp(secret: str, otp: str, count: int) -> bool:
class NotifySetting:
"""Store notify setting for one user."""
secret = attr.ib(type=str, factory=_generate_secret) # not persistent
secret = attr.ib(type=str, factory=_generate_secret) # not persistent
counter = attr.ib(type=int, factory=_generate_random) # not persistent
notify_service = attr.ib(type=Optional[str], default=None)
target = attr.ib(type=Optional[str], default=None)
@ -76,18 +86,19 @@ class NotifySetting:
_UsersDict = Dict[str, NotifySetting]
@MULTI_FACTOR_AUTH_MODULES.register('notify')
@MULTI_FACTOR_AUTH_MODULES.register("notify")
class NotifyAuthModule(MultiFactorAuthModule):
"""Auth module send hmac-based one time password by notify service."""
DEFAULT_TITLE = 'Notify One-Time Password'
DEFAULT_TITLE = "Notify One-Time Password"
def __init__(self, hass: HomeAssistant, config: Dict[str, Any]) -> None:
"""Initialize the user data store."""
super().__init__(hass, config)
self._user_settings = None # type: Optional[_UsersDict]
self._user_store = hass.helpers.storage.Store(
STORAGE_VERSION, STORAGE_KEY, private=True)
STORAGE_VERSION, STORAGE_KEY, private=True
)
self._include = config.get(CONF_INCLUDE, [])
self._exclude = config.get(CONF_EXCLUDE, [])
self._message_template = config[CONF_MESSAGE]
@ -119,22 +130,27 @@ class NotifyAuthModule(MultiFactorAuthModule):
if self._user_settings is None:
return
await self._user_store.async_save({STORAGE_USERS: {
user_id: attr.asdict(
notify_setting, filter=attr.filters.exclude(
attr.fields(NotifySetting).secret,
attr.fields(NotifySetting).counter,
))
for user_id, notify_setting
in self._user_settings.items()
}})
await self._user_store.async_save(
{
STORAGE_USERS: {
user_id: attr.asdict(
notify_setting,
filter=attr.filters.exclude(
attr.fields(NotifySetting).secret,
attr.fields(NotifySetting).counter,
),
)
for user_id, notify_setting in self._user_settings.items()
}
}
)
@callback
def aync_get_available_notify_services(self) -> List[str]:
"""Return list of notify services."""
unordered_services = set()
for service in self.hass.services.async_services().get('notify', {}):
for service in self.hass.services.async_services().get("notify", {}):
if service not in self._exclude:
unordered_services.add(service)
@ -149,8 +165,8 @@ class NotifyAuthModule(MultiFactorAuthModule):
Mfa module should extend SetupFlow
"""
return NotifySetupFlow(
self, self.input_schema, user_id,
self.aync_get_available_notify_services())
self, self.input_schema, user_id, self.aync_get_available_notify_services()
)
async def async_setup_user(self, user_id: str, setup_data: Any) -> Any:
"""Set up auth module for user."""
@ -159,8 +175,8 @@ class NotifyAuthModule(MultiFactorAuthModule):
assert self._user_settings is not None
self._user_settings[user_id] = NotifySetting(
notify_service=setup_data.get('notify_service'),
target=setup_data.get('target'),
notify_service=setup_data.get("notify_service"),
target=setup_data.get("target"),
)
await self._async_save()
@ -182,8 +198,7 @@ class NotifyAuthModule(MultiFactorAuthModule):
return user_id in self._user_settings
async def async_validate(
self, user_id: str, user_input: Dict[str, Any]) -> bool:
async def async_validate(self, user_id: str, user_input: Dict[str, Any]) -> bool:
"""Return True if validation passed."""
if self._user_settings is None:
await self._async_load()
@ -195,9 +210,11 @@ class NotifyAuthModule(MultiFactorAuthModule):
# user_input has been validate in caller
return await self.hass.async_add_executor_job(
_verify_otp, notify_setting.secret,
user_input.get(INPUT_FIELD_CODE, ''),
notify_setting.counter)
_verify_otp,
notify_setting.secret,
user_input.get(INPUT_FIELD_CODE, ""),
notify_setting.counter,
)
async def async_initialize_login_mfa_step(self, user_id: str) -> None:
"""Generate code and notify user."""
@ -207,7 +224,7 @@ class NotifyAuthModule(MultiFactorAuthModule):
notify_setting = self._user_settings.get(user_id, None)
if notify_setting is None:
raise ValueError('Cannot find user_id')
raise ValueError("Cannot find user_id")
def generate_secret_and_one_time_password() -> str:
"""Generate and send one time password."""
@ -215,11 +232,11 @@ class NotifyAuthModule(MultiFactorAuthModule):
# secret and counter are not persistent
notify_setting.secret = _generate_secret()
notify_setting.counter = _generate_random()
return _generate_otp(
notify_setting.secret, notify_setting.counter)
return _generate_otp(notify_setting.secret, notify_setting.counter)
code = await self.hass.async_add_executor_job(
generate_secret_and_one_time_password)
generate_secret_and_one_time_password
)
await self.async_notify_user(user_id, code)
@ -231,105 +248,107 @@ class NotifyAuthModule(MultiFactorAuthModule):
notify_setting = self._user_settings.get(user_id, None)
if notify_setting is None:
_LOGGER.error('Cannot find user %s', user_id)
_LOGGER.error("Cannot find user %s", user_id)
return
await self.async_notify( # type: ignore
code, notify_setting.notify_service, notify_setting.target)
await self.async_notify( # type: ignore
code, notify_setting.notify_service, notify_setting.target
)
async def async_notify(self, code: str, notify_service: str,
target: Optional[str] = None) -> None:
async def async_notify(
self, code: str, notify_service: str, target: Optional[str] = None
) -> None:
"""Send code by notify service."""
data = {'message': self._message_template.format(code)}
data = {"message": self._message_template.format(code)}
if target:
data['target'] = [target]
data["target"] = [target]
await self.hass.services.async_call('notify', notify_service, data)
await self.hass.services.async_call("notify", notify_service, data)
class NotifySetupFlow(SetupFlow):
"""Handler for the setup flow."""
def __init__(self, auth_module: NotifyAuthModule,
setup_schema: vol.Schema,
user_id: str,
available_notify_services: List[str]) -> None:
def __init__(
self,
auth_module: NotifyAuthModule,
setup_schema: vol.Schema,
user_id: str,
available_notify_services: List[str],
) -> None:
"""Initialize the setup flow."""
super().__init__(auth_module, setup_schema, user_id)
# to fix typing complaint
self._auth_module = auth_module # type: NotifyAuthModule
self._available_notify_services = available_notify_services
self._secret = None # type: Optional[str]
self._count = None # type: Optional[int]
self._count = None # type: Optional[int]
self._notify_service = None # type: Optional[str]
self._target = None # type: Optional[str]
async def async_step_init(
self, user_input: Optional[Dict[str, str]] = None) \
-> Dict[str, Any]:
self, user_input: Optional[Dict[str, str]] = None
) -> Dict[str, Any]:
"""Let user select available notify services."""
errors = {} # type: Dict[str, str]
hass = self._auth_module.hass
if user_input:
self._notify_service = user_input['notify_service']
self._target = user_input.get('target')
self._notify_service = user_input["notify_service"]
self._target = user_input.get("target")
self._secret = await hass.async_add_executor_job(_generate_secret)
self._count = await hass.async_add_executor_job(_generate_random)
return await self.async_step_setup()
if not self._available_notify_services:
return self.async_abort(reason='no_available_service')
return self.async_abort(reason="no_available_service")
schema = OrderedDict() # type: Dict[str, Any]
schema['notify_service'] = vol.In(self._available_notify_services)
schema['target'] = vol.Optional(str)
schema["notify_service"] = vol.In(self._available_notify_services)
schema["target"] = vol.Optional(str)
return self.async_show_form(
step_id='init',
data_schema=vol.Schema(schema),
errors=errors
step_id="init", data_schema=vol.Schema(schema), errors=errors
)
async def async_step_setup(
self, user_input: Optional[Dict[str, str]] = None) \
-> Dict[str, Any]:
self, user_input: Optional[Dict[str, str]] = None
) -> Dict[str, Any]:
"""Verify user can recevie one-time password."""
errors = {} # type: Dict[str, str]
hass = self._auth_module.hass
if user_input:
verified = await hass.async_add_executor_job(
_verify_otp, self._secret, user_input['code'], self._count)
_verify_otp, self._secret, user_input["code"], self._count
)
if verified:
await self._auth_module.async_setup_user(
self._user_id, {
'notify_service': self._notify_service,
'target': self._target,
})
return self.async_create_entry(
title=self._auth_module.name,
data={}
self._user_id,
{"notify_service": self._notify_service, "target": self._target},
)
return self.async_create_entry(title=self._auth_module.name, data={})
errors['base'] = 'invalid_code'
errors["base"] = "invalid_code"
# generate code every time, no retry logic
assert self._secret and self._count
code = await hass.async_add_executor_job(
_generate_otp, self._secret, self._count)
_generate_otp, self._secret, self._count
)
assert self._notify_service
try:
await self._auth_module.async_notify(
code, self._notify_service, self._target)
code, self._notify_service, self._target
)
except ServiceNotFound:
return self.async_abort(reason='notify_service_not_exist')
return self.async_abort(reason="notify_service_not_exist")
return self.async_show_form(
step_id='setup',
step_id="setup",
data_schema=self._setup_schema,
description_placeholders={'notify_service': self._notify_service},
description_placeholders={"notify_service": self._notify_service},
errors=errors,
)

View file

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

View file

@ -11,9 +11,9 @@ from . import permissions as perm_mdl
from .const import GROUP_ID_ADMIN
from .util import generate_secret
TOKEN_TYPE_NORMAL = 'normal'
TOKEN_TYPE_SYSTEM = 'system'
TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN = 'long_lived_access_token'
TOKEN_TYPE_NORMAL = "normal"
TOKEN_TYPE_SYSTEM = "system"
TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN = "long_lived_access_token"
@attr.s(slots=True)
@ -32,7 +32,7 @@ class User:
name = attr.ib(type=str) # type: Optional[str]
perm_lookup = attr.ib(
type=perm_mdl.PermissionLookup, cmp=False,
type=perm_mdl.PermissionLookup, cmp=False
) # type: perm_mdl.PermissionLookup
id = attr.ib(type=str, factory=lambda: uuid.uuid4().hex)
is_owner = attr.ib(type=bool, default=False)
@ -42,9 +42,7 @@ class User:
groups = attr.ib(type=List, factory=list, cmp=False) # type: List[Group]
# List of credentials of a user.
credentials = attr.ib(
type=list, factory=list, cmp=False
) # type: List[Credentials]
credentials = attr.ib(type=list, factory=list, cmp=False) # type: List[Credentials]
# Tokens associated with a user.
refresh_tokens = attr.ib(
@ -52,10 +50,7 @@ class User:
) # type: Dict[str, RefreshToken]
_permissions = attr.ib(
type=Optional[perm_mdl.PolicyPermissions],
init=False,
cmp=False,
default=None,
type=Optional[perm_mdl.PolicyPermissions], init=False, cmp=False, default=None
)
@property
@ -68,9 +63,9 @@ class User:
return self._permissions
self._permissions = perm_mdl.PolicyPermissions(
perm_mdl.merge_policies([
group.policy for group in self.groups]),
self.perm_lookup)
perm_mdl.merge_policies([group.policy for group in self.groups]),
self.perm_lookup,
)
return self._permissions
@ -80,8 +75,7 @@ class User:
if self.is_owner:
return True
return self.is_active and any(
gr.id == GROUP_ID_ADMIN for gr in self.groups)
return self.is_active and any(gr.id == GROUP_ID_ADMIN for gr in self.groups)
def invalidate_permission_cache(self) -> None:
"""Invalidate permission cache."""
@ -97,10 +91,13 @@ class RefreshToken:
access_token_expiration = attr.ib(type=timedelta)
client_name = attr.ib(type=Optional[str], default=None)
client_icon = attr.ib(type=Optional[str], default=None)
token_type = attr.ib(type=str, default=TOKEN_TYPE_NORMAL,
validator=attr.validators.in_((
TOKEN_TYPE_NORMAL, TOKEN_TYPE_SYSTEM,
TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN)))
token_type = attr.ib(
type=str,
default=TOKEN_TYPE_NORMAL,
validator=attr.validators.in_(
(TOKEN_TYPE_NORMAL, TOKEN_TYPE_SYSTEM, TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN)
),
)
id = attr.ib(type=str, factory=lambda: uuid.uuid4().hex)
created_at = attr.ib(type=datetime, factory=dt_util.utcnow)
token = attr.ib(type=str, factory=lambda: generate_secret(64))
@ -124,5 +121,4 @@ class Credentials:
is_new = attr.ib(type=bool, default=True)
UserMeta = NamedTuple("UserMeta",
[('name', Optional[str]), ('is_active', bool)])
UserMeta = NamedTuple("UserMeta", [("name", Optional[str]), ("is_active", bool)])

View file

@ -1,8 +1,17 @@
"""Permissions for Home Assistant."""
import logging
from typing import ( # noqa: F401
cast, Any, Callable, Dict, List, Mapping, Set, Tuple, Union,
TYPE_CHECKING)
cast,
Any,
Callable,
Dict,
List,
Mapping,
Set,
Tuple,
Union,
TYPE_CHECKING,
)
import voluptuous as vol
@ -14,9 +23,7 @@ from .merge import merge_policies # noqa
from .util import test_all
POLICY_SCHEMA = vol.Schema({
vol.Optional(CAT_ENTITIES): ENTITY_POLICY_SCHEMA
})
POLICY_SCHEMA = vol.Schema({vol.Optional(CAT_ENTITIES): ENTITY_POLICY_SCHEMA})
_LOGGER = logging.getLogger(__name__)
@ -47,8 +54,7 @@ class AbstractPermissions:
class PolicyPermissions(AbstractPermissions):
"""Handle permissions."""
def __init__(self, policy: PolicyType,
perm_lookup: PermissionLookup) -> None:
def __init__(self, policy: PolicyType, perm_lookup: PermissionLookup) -> None:
"""Initialize the permission class."""
self._policy = policy
self._perm_lookup = perm_lookup
@ -59,14 +65,12 @@ class PolicyPermissions(AbstractPermissions):
def _entity_func(self) -> Callable[[str, str], bool]:
"""Return a function that can test entity access."""
return compile_entities(self._policy.get(CAT_ENTITIES),
self._perm_lookup)
return compile_entities(self._policy.get(CAT_ENTITIES), self._perm_lookup)
def __eq__(self, other: Any) -> bool:
"""Equals check."""
# pylint: disable=protected-access
return (isinstance(other, PolicyPermissions) and
other._policy == self._policy)
return isinstance(other, PolicyPermissions) and other._policy == self._policy
class _OwnerPermissions(AbstractPermissions):

View file

@ -1,8 +1,8 @@
"""Permission constants."""
CAT_ENTITIES = 'entities'
CAT_CONFIG_ENTRIES = 'config_entries'
SUBCAT_ALL = 'all'
CAT_ENTITIES = "entities"
CAT_CONFIG_ENTRIES = "config_entries"
SUBCAT_ALL = "all"
POLICY_READ = 'read'
POLICY_CONTROL = 'control'
POLICY_EDIT = 'edit'
POLICY_READ = "read"
POLICY_CONTROL = "control"
POLICY_EDIT = "edit"

View file

@ -7,51 +7,59 @@ import voluptuous as vol
from .const import SUBCAT_ALL, POLICY_READ, POLICY_CONTROL, POLICY_EDIT
from .models import PermissionLookup
from .types import CategoryType, SubCategoryDict, ValueType
# pylint: disable=unused-import
from .util import SubCatLookupType, lookup_all, compile_policy # noqa
SINGLE_ENTITY_SCHEMA = vol.Any(True, vol.Schema({
vol.Optional(POLICY_READ): True,
vol.Optional(POLICY_CONTROL): True,
vol.Optional(POLICY_EDIT): True,
}))
SINGLE_ENTITY_SCHEMA = vol.Any(
True,
vol.Schema(
{
vol.Optional(POLICY_READ): True,
vol.Optional(POLICY_CONTROL): True,
vol.Optional(POLICY_EDIT): True,
}
),
)
ENTITY_DOMAINS = 'domains'
ENTITY_AREAS = 'area_ids'
ENTITY_DEVICE_IDS = 'device_ids'
ENTITY_ENTITY_IDS = 'entity_ids'
ENTITY_DOMAINS = "domains"
ENTITY_AREAS = "area_ids"
ENTITY_DEVICE_IDS = "device_ids"
ENTITY_ENTITY_IDS = "entity_ids"
ENTITY_VALUES_SCHEMA = vol.Any(True, vol.Schema({
str: SINGLE_ENTITY_SCHEMA
}))
ENTITY_VALUES_SCHEMA = vol.Any(True, vol.Schema({str: SINGLE_ENTITY_SCHEMA}))
ENTITY_POLICY_SCHEMA = vol.Any(True, vol.Schema({
vol.Optional(SUBCAT_ALL): SINGLE_ENTITY_SCHEMA,
vol.Optional(ENTITY_AREAS): ENTITY_VALUES_SCHEMA,
vol.Optional(ENTITY_DEVICE_IDS): ENTITY_VALUES_SCHEMA,
vol.Optional(ENTITY_DOMAINS): ENTITY_VALUES_SCHEMA,
vol.Optional(ENTITY_ENTITY_IDS): ENTITY_VALUES_SCHEMA,
}))
ENTITY_POLICY_SCHEMA = vol.Any(
True,
vol.Schema(
{
vol.Optional(SUBCAT_ALL): SINGLE_ENTITY_SCHEMA,
vol.Optional(ENTITY_AREAS): ENTITY_VALUES_SCHEMA,
vol.Optional(ENTITY_DEVICE_IDS): ENTITY_VALUES_SCHEMA,
vol.Optional(ENTITY_DOMAINS): ENTITY_VALUES_SCHEMA,
vol.Optional(ENTITY_ENTITY_IDS): ENTITY_VALUES_SCHEMA,
}
),
)
def _lookup_domain(perm_lookup: PermissionLookup,
domains_dict: SubCategoryDict,
entity_id: str) -> Optional[ValueType]:
def _lookup_domain(
perm_lookup: PermissionLookup, domains_dict: SubCategoryDict, entity_id: str
) -> Optional[ValueType]:
"""Look up entity permissions by domain."""
return domains_dict.get(entity_id.split(".", 1)[0])
def _lookup_area(perm_lookup: PermissionLookup, area_dict: SubCategoryDict,
entity_id: str) -> Optional[ValueType]:
def _lookup_area(
perm_lookup: PermissionLookup, area_dict: SubCategoryDict, entity_id: str
) -> Optional[ValueType]:
"""Look up entity permissions by area."""
entity_entry = perm_lookup.entity_registry.async_get(entity_id)
if entity_entry is None or entity_entry.device_id is None:
return None
device_entry = perm_lookup.device_registry.async_get(
entity_entry.device_id
)
device_entry = perm_lookup.device_registry.async_get(entity_entry.device_id)
if device_entry is None or device_entry.area_id is None:
return None
@ -59,9 +67,9 @@ def _lookup_area(perm_lookup: PermissionLookup, area_dict: SubCategoryDict,
return area_dict.get(device_entry.area_id)
def _lookup_device(perm_lookup: PermissionLookup,
devices_dict: SubCategoryDict,
entity_id: str) -> Optional[ValueType]:
def _lookup_device(
perm_lookup: PermissionLookup, devices_dict: SubCategoryDict, entity_id: str
) -> Optional[ValueType]:
"""Look up entity permissions by device."""
entity_entry = perm_lookup.entity_registry.async_get(entity_id)
@ -71,15 +79,16 @@ def _lookup_device(perm_lookup: PermissionLookup,
return devices_dict.get(entity_entry.device_id)
def _lookup_entity_id(perm_lookup: PermissionLookup,
entities_dict: SubCategoryDict,
entity_id: str) -> Optional[ValueType]:
def _lookup_entity_id(
perm_lookup: PermissionLookup, entities_dict: SubCategoryDict, entity_id: str
) -> Optional[ValueType]:
"""Look up entity permission by entity id."""
return entities_dict.get(entity_id)
def compile_entities(policy: CategoryType, perm_lookup: PermissionLookup) \
-> Callable[[str, str], bool]:
def compile_entities(
policy: CategoryType, perm_lookup: PermissionLookup
) -> Callable[[str, str], bool]:
"""Compile policy into a function that tests policy."""
subcategories = OrderedDict() # type: SubCatLookupType
subcategories[ENTITY_ENTITY_IDS] = _lookup_entity_id

View file

@ -1,6 +1,5 @@
"""Merging of policies."""
from typing import ( # noqa: F401
cast, Dict, List, Set)
from typing import cast, Dict, List, Set # noqa: F401
from .types import PolicyType, CategoryType
@ -14,8 +13,9 @@ def merge_policies(policies: List[PolicyType]) -> PolicyType:
if category in seen:
continue
seen.add(category)
new_policy[category] = _merge_policies([
policy.get(category) for policy in policies])
new_policy[category] = _merge_policies(
[policy.get(category) for policy in policies]
)
cast(PolicyType, new_policy)
return new_policy

View file

@ -5,17 +5,13 @@ import attr
if TYPE_CHECKING:
# pylint: disable=unused-import
from homeassistant.helpers import ( # noqa
entity_registry as ent_reg,
)
from homeassistant.helpers import ( # noqa
device_registry as dev_reg,
)
from homeassistant.helpers import entity_registry as ent_reg # noqa
from homeassistant.helpers import device_registry as dev_reg # noqa
@attr.s(slots=True)
class PermissionLookup:
"""Class to hold data for permission lookups."""
entity_registry = attr.ib(type='ent_reg.EntityRegistry')
device_registry = attr.ib(type='dev_reg.DeviceRegistry')
entity_registry = attr.ib(type="ent_reg.EntityRegistry")
device_registry = attr.ib(type="dev_reg.DeviceRegistry")

View file

@ -1,18 +1,8 @@
"""System policies."""
from .const import CAT_ENTITIES, SUBCAT_ALL, POLICY_READ
ADMIN_POLICY = {
CAT_ENTITIES: True,
}
ADMIN_POLICY = {CAT_ENTITIES: True}
USER_POLICY = {
CAT_ENTITIES: True,
}
USER_POLICY = {CAT_ENTITIES: True}
READ_ONLY_POLICY = {
CAT_ENTITIES: {
SUBCAT_ALL: {
POLICY_READ: True
}
}
}
READ_ONLY_POLICY = {CAT_ENTITIES: {SUBCAT_ALL: {POLICY_READ: True}}}

View file

@ -7,17 +7,13 @@ ValueType = Union[
# Example: entities.all = { read: true, control: true }
Mapping[str, bool],
bool,
None
None,
]
# Example: entities.domains = { light: … }
SubCategoryDict = Mapping[str, ValueType]
SubCategoryType = Union[
SubCategoryDict,
bool,
None
]
SubCategoryType = Union[SubCategoryDict, bool, None]
CategoryType = Union[
# Example: entities.domains
@ -25,7 +21,7 @@ CategoryType = Union[
# Example: entities.all
Mapping[str, ValueType],
bool,
None
None,
]
# Example: { entities: … }

View file

@ -7,28 +7,28 @@ from .const import SUBCAT_ALL
from .models import PermissionLookup
from .types import CategoryType, SubCategoryDict, ValueType
LookupFunc = Callable[[PermissionLookup, SubCategoryDict, str],
Optional[ValueType]]
LookupFunc = Callable[[PermissionLookup, SubCategoryDict, str], Optional[ValueType]]
SubCatLookupType = Dict[str, LookupFunc]
def lookup_all(perm_lookup: PermissionLookup, lookup_dict: SubCategoryDict,
object_id: str) -> ValueType:
def lookup_all(
perm_lookup: PermissionLookup, lookup_dict: SubCategoryDict, object_id: str
) -> ValueType:
"""Look up permission for all."""
# In case of ALL category, lookup_dict IS the schema.
return cast(ValueType, lookup_dict)
def compile_policy(
policy: CategoryType, subcategories: SubCatLookupType,
perm_lookup: PermissionLookup
) -> Callable[[str, str], bool]: # noqa
policy: CategoryType, subcategories: SubCatLookupType, perm_lookup: PermissionLookup
) -> Callable[[str, str], bool]: # noqa
"""Compile policy into a function that tests policy.
Subcategories are mapping key -> lookup function, ordered by highest
priority first.
"""
# None, False, empty dict
if not policy:
def apply_policy_deny_all(entity_id: str, key: str) -> bool:
"""Decline all."""
return False
@ -36,6 +36,7 @@ def compile_policy(
return apply_policy_deny_all
if policy is True:
def apply_policy_allow_all(entity_id: str, key: str) -> bool:
"""Approve all."""
return True
@ -54,8 +55,7 @@ def compile_policy(
return lambda object_id, key: True
if lookup_value is not None:
funcs.append(_gen_dict_test_func(
perm_lookup, lookup_func, lookup_value))
funcs.append(_gen_dict_test_func(perm_lookup, lookup_func, lookup_value))
if len(funcs) == 1:
func = funcs[0]
@ -79,15 +79,13 @@ def compile_policy(
def _gen_dict_test_func(
perm_lookup: PermissionLookup,
lookup_func: LookupFunc,
lookup_dict: SubCategoryDict
) -> Callable[[str, str], Optional[bool]]: # noqa
perm_lookup: PermissionLookup, lookup_func: LookupFunc, lookup_dict: SubCategoryDict
) -> Callable[[str, str], Optional[bool]]: # noqa
"""Generate a lookup function."""
def test_value(object_id: str, key: str) -> Optional[bool]:
"""Test if permission is allowed based on the keys."""
schema = lookup_func(
perm_lookup, lookup_dict, object_id) # type: ValueType
schema = lookup_func(perm_lookup, lookup_dict, object_id) # type: ValueType
if schema is None or isinstance(schema, bool):
return schema

View file

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

View file

@ -19,15 +19,16 @@ CONF_COMMAND = "command"
CONF_ARGS = "args"
CONF_META = "meta"
CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend({
vol.Required(CONF_COMMAND): vol.All(
str,
os.path.normpath,
msg="must be an absolute path"
),
vol.Optional(CONF_ARGS, default=None): vol.Any(vol.DefaultTo(list), [str]),
vol.Optional(CONF_META, default=False): bool,
}, extra=vol.PREVENT_EXTRA)
CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend(
{
vol.Required(CONF_COMMAND): vol.All(
str, os.path.normpath, msg="must be an absolute path"
),
vol.Optional(CONF_ARGS, default=None): vol.Any(vol.DefaultTo(list), [str]),
vol.Optional(CONF_META, default=False): bool,
},
extra=vol.PREVENT_EXTRA,
)
_LOGGER = logging.getLogger(__name__)
@ -60,29 +61,27 @@ class CommandLineAuthProvider(AuthProvider):
async def async_validate_login(self, username: str, password: str) -> None:
"""Validate a username and password."""
env = {
"username": username,
"password": password,
}
env = {"username": username, "password": password}
try:
# pylint: disable=no-member
process = await asyncio.subprocess.create_subprocess_exec(
self.config[CONF_COMMAND], *self.config[CONF_ARGS],
self.config[CONF_COMMAND],
*self.config[CONF_ARGS],
env=env,
stdout=asyncio.subprocess.PIPE
if self.config[CONF_META] else None,
stdout=asyncio.subprocess.PIPE if self.config[CONF_META] else None,
)
stdout, _ = (await process.communicate())
stdout, _ = await process.communicate()
except OSError as err:
# happens when command doesn't exist or permission is denied
_LOGGER.error("Error while authenticating %r: %s",
username, err)
_LOGGER.error("Error while authenticating %r: %s", username, err)
raise InvalidAuthError
if process.returncode != 0:
_LOGGER.error("User %r failed to authenticate, command exited "
"with code %d.",
username, process.returncode)
_LOGGER.error(
"User %r failed to authenticate, command exited " "with code %d.",
username,
process.returncode,
)
raise InvalidAuthError
if self.config[CONF_META]:
@ -103,7 +102,7 @@ class CommandLineAuthProvider(AuthProvider):
self._user_meta[username] = meta
async def async_get_or_create_credentials(
self, flow_result: Dict[str, str]
self, flow_result: Dict[str, str]
) -> Credentials:
"""Get credentials based on the flow result."""
username = flow_result["username"]
@ -112,29 +111,24 @@ class CommandLineAuthProvider(AuthProvider):
return credential
# Create new credentials.
return self.async_create_credentials({
"username": username,
})
return self.async_create_credentials({"username": username})
async def async_user_meta_for_credentials(
self, credentials: Credentials
self, credentials: Credentials
) -> UserMeta:
"""Return extra user metadata for credentials.
Currently, only name is supported.
"""
meta = self._user_meta.get(credentials.data["username"], {})
return UserMeta(
name=meta.get("name"),
is_active=True,
)
return UserMeta(name=meta.get("name"), is_active=True)
class CommandLineLoginFlow(LoginFlow):
"""Handler for the login flow."""
async def async_step_init(
self, user_input: Optional[Dict[str, str]] = None
self, user_input: Optional[Dict[str, str]] = None
) -> Dict[str, Any]:
"""Handle the step of the form."""
errors = {}
@ -142,10 +136,9 @@ class CommandLineLoginFlow(LoginFlow):
if user_input is not None:
user_input["username"] = user_input["username"].strip()
try:
await cast(CommandLineAuthProvider, self._auth_provider) \
.async_validate_login(
user_input["username"], user_input["password"]
)
await cast(
CommandLineAuthProvider, self._auth_provider
).async_validate_login(user_input["username"], user_input["password"])
except InvalidAuthError:
errors["base"] = "invalid_auth"
@ -158,7 +151,5 @@ class CommandLineLoginFlow(LoginFlow):
schema["password"] = str
return self.async_show_form(
step_id="init",
data_schema=vol.Schema(schema),
errors=errors,
step_id="init", data_schema=vol.Schema(schema), errors=errors
)

View file

@ -19,14 +19,13 @@ from ..models import Credentials, UserMeta
STORAGE_VERSION = 1
STORAGE_KEY = 'auth_provider.homeassistant'
STORAGE_KEY = "auth_provider.homeassistant"
def _disallow_id(conf: Dict[str, Any]) -> Dict[str, Any]:
"""Disallow ID in config."""
if CONF_ID in conf:
raise vol.Invalid(
'ID is not allowed for the homeassistant auth provider.')
raise vol.Invalid("ID is not allowed for the homeassistant auth provider.")
return conf
@ -51,8 +50,9 @@ class Data:
def __init__(self, hass: HomeAssistant) -> None:
"""Initialize the user data store."""
self.hass = hass
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY,
private=True)
self._store = hass.helpers.storage.Store(
STORAGE_VERSION, STORAGE_KEY, private=True
)
self._data = None # type: Optional[Dict[str, Any]]
# Legacy mode will allow usernames to start/end with whitespace
# and will compare usernames case-insensitive.
@ -72,14 +72,12 @@ class Data:
data = await self._store.async_load()
if data is None:
data = {
'users': []
}
data = {"users": []}
seen = set() # type: Set[str]
for user in data['users']:
username = user['username']
for user in data["users"]:
username = user["username"]
# check if we have duplicates
folded = username.casefold()
@ -90,7 +88,9 @@ class Data:
logging.getLogger(__name__).warning(
"Home Assistant auth provider is running in legacy mode "
"because we detected usernames that are case-insensitive"
"equivalent. Please change the username: '%s'.", username)
"equivalent. Please change the username: '%s'.",
username,
)
break
@ -103,7 +103,9 @@ class Data:
logging.getLogger(__name__).warning(
"Home Assistant auth provider is running in legacy mode "
"because we detected usernames that start or end in a "
"space. Please change the username: '%s'.", username)
"space. Please change the username: '%s'.",
username,
)
break
@ -112,7 +114,7 @@ class Data:
@property
def users(self) -> List[Dict[str, str]]:
"""Return users."""
return self._data['users'] # type: ignore
return self._data["users"] # type: ignore
def validate_login(self, username: str, password: str) -> None:
"""Validate a username and password.
@ -120,32 +122,30 @@ class Data:
Raises InvalidAuth if auth invalid.
"""
username = self.normalize_username(username)
dummy = b'$2b$12$CiuFGszHx9eNHxPuQcwBWez4CwDTOcLTX5CbOpV6gef2nYuXkY7BO'
dummy = b"$2b$12$CiuFGszHx9eNHxPuQcwBWez4CwDTOcLTX5CbOpV6gef2nYuXkY7BO"
found = None
# Compare all users to avoid timing attacks.
for user in self.users:
if self.normalize_username(user['username']) == username:
if self.normalize_username(user["username"]) == username:
found = user
if found is None:
# check a hash to make timing the same as if user was found
bcrypt.checkpw(b'foo',
dummy)
bcrypt.checkpw(b"foo", dummy)
raise InvalidAuth
user_hash = base64.b64decode(found['password'])
user_hash = base64.b64decode(found["password"])
# bcrypt.checkpw is timing-safe
if not bcrypt.checkpw(password.encode(),
user_hash):
if not bcrypt.checkpw(password.encode(), user_hash):
raise InvalidAuth
# pylint: disable=no-self-use
def hash_password(self, password: str, for_storage: bool = False) -> bytes:
"""Encode a password."""
hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12)) \
# type: bytes
hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12))
# type: bytes
if for_storage:
hashed = base64.b64encode(hashed)
return hashed
@ -154,14 +154,17 @@ class Data:
"""Add a new authenticated user/pass."""
username = self.normalize_username(username)
if any(self.normalize_username(user['username']) == username
for user in self.users):
if any(
self.normalize_username(user["username"]) == username for user in self.users
):
raise InvalidUser
self.users.append({
'username': username,
'password': self.hash_password(password, True).decode(),
})
self.users.append(
{
"username": username,
"password": self.hash_password(password, True).decode(),
}
)
@callback
def async_remove_auth(self, username: str) -> None:
@ -170,7 +173,7 @@ class Data:
index = None
for i, user in enumerate(self.users):
if self.normalize_username(user['username']) == username:
if self.normalize_username(user["username"]) == username:
index = i
break
@ -187,9 +190,8 @@ class Data:
username = self.normalize_username(username)
for user in self.users:
if self.normalize_username(user['username']) == username:
user['password'] = self.hash_password(
new_password, True).decode()
if self.normalize_username(user["username"]) == username:
user["password"] = self.hash_password(new_password, True).decode()
break
else:
raise InvalidUser
@ -199,11 +201,11 @@ class Data:
await self._store.async_save(self._data)
@AUTH_PROVIDERS.register('homeassistant')
@AUTH_PROVIDERS.register("homeassistant")
class HassAuthProvider(AuthProvider):
"""Auth provider based on a local storage of users in HASS config dir."""
DEFAULT_TITLE = 'Home Assistant Local'
DEFAULT_TITLE = "Home Assistant Local"
def __init__(self, *args: Any, **kwargs: Any) -> None:
"""Initialize an Home Assistant auth provider."""
@ -221,8 +223,7 @@ class HassAuthProvider(AuthProvider):
await data.async_load()
self.data = data
async def async_login_flow(
self, context: Optional[Dict]) -> LoginFlow:
async def async_login_flow(self, context: Optional[Dict]) -> LoginFlow:
"""Return a flow to login."""
return HassLoginFlow(self)
@ -233,41 +234,41 @@ class HassAuthProvider(AuthProvider):
assert self.data is not None
await self.hass.async_add_executor_job(
self.data.validate_login, username, password)
self.data.validate_login, username, password
)
async def async_get_or_create_credentials(
self, flow_result: Dict[str, str]) -> Credentials:
self, flow_result: Dict[str, str]
) -> Credentials:
"""Get credentials based on the flow result."""
if self.data is None:
await self.async_initialize()
assert self.data is not None
norm_username = self.data.normalize_username
username = norm_username(flow_result['username'])
username = norm_username(flow_result["username"])
for credential in await self.async_credentials():
if norm_username(credential.data['username']) == username:
if norm_username(credential.data["username"]) == username:
return credential
# Create new credentials.
return self.async_create_credentials({
'username': username
})
return self.async_create_credentials({"username": username})
async def async_user_meta_for_credentials(
self, credentials: Credentials) -> UserMeta:
self, credentials: Credentials
) -> UserMeta:
"""Get extra info for this credential."""
return UserMeta(name=credentials.data['username'], is_active=True)
return UserMeta(name=credentials.data["username"], is_active=True)
async def async_will_remove_credentials(
self, credentials: Credentials) -> None:
async def async_will_remove_credentials(self, credentials: Credentials) -> None:
"""When credentials get removed, also remove the auth."""
if self.data is None:
await self.async_initialize()
assert self.data is not None
try:
self.data.async_remove_auth(credentials.data['username'])
self.data.async_remove_auth(credentials.data["username"])
await self.data.async_save()
except InvalidUser:
# Can happen if somehow we didn't clean up a credential
@ -278,29 +279,27 @@ class HassLoginFlow(LoginFlow):
"""Handler for the login flow."""
async def async_step_init(
self, user_input: Optional[Dict[str, str]] = None) \
-> Dict[str, Any]:
self, user_input: Optional[Dict[str, str]] = None
) -> Dict[str, Any]:
"""Handle the step of the form."""
errors = {}
if user_input is not None:
try:
await cast(HassAuthProvider, self._auth_provider)\
.async_validate_login(user_input['username'],
user_input['password'])
await cast(HassAuthProvider, self._auth_provider).async_validate_login(
user_input["username"], user_input["password"]
)
except InvalidAuth:
errors['base'] = 'invalid_auth'
errors["base"] = "invalid_auth"
if not errors:
user_input.pop('password')
user_input.pop("password")
return await self.async_finish(user_input)
schema = OrderedDict() # type: Dict[str, type]
schema['username'] = str
schema['password'] = str
schema["username"] = str
schema["password"] = str
return self.async_show_form(
step_id='init',
data_schema=vol.Schema(schema),
errors=errors,
step_id="init", data_schema=vol.Schema(schema), errors=errors
)

View file

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

View file

@ -16,27 +16,26 @@ from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, LoginFlow
from .. import AuthManager
from ..models import Credentials, UserMeta, User
AUTH_PROVIDER_TYPE = 'legacy_api_password'
CONF_API_PASSWORD = 'api_password'
AUTH_PROVIDER_TYPE = "legacy_api_password"
CONF_API_PASSWORD = "api_password"
CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend({
vol.Required(CONF_API_PASSWORD): cv.string,
}, extra=vol.PREVENT_EXTRA)
CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend(
{vol.Required(CONF_API_PASSWORD): cv.string}, extra=vol.PREVENT_EXTRA
)
LEGACY_USER_NAME = 'Legacy API password user'
LEGACY_USER_NAME = "Legacy API password user"
class InvalidAuthError(HomeAssistantError):
"""Raised when submitting invalid authentication."""
async def async_validate_password(hass: HomeAssistant, password: str)\
-> Optional[User]:
async def async_validate_password(hass: HomeAssistant, password: str) -> Optional[User]:
"""Return a user if password is valid. None if not."""
auth = cast(AuthManager, hass.auth) # type: ignore
providers = auth.get_auth_providers(AUTH_PROVIDER_TYPE)
if not providers:
raise ValueError('Legacy API password provider not found')
raise ValueError("Legacy API password provider not found")
try:
provider = cast(LegacyApiPasswordAuthProvider, providers[0])
@ -52,7 +51,7 @@ async def async_validate_password(hass: HomeAssistant, password: str)\
class LegacyApiPasswordAuthProvider(AuthProvider):
"""An auth provider support legacy api_password."""
DEFAULT_TITLE = 'Legacy API Password'
DEFAULT_TITLE = "Legacy API Password"
@property
def api_password(self) -> str:
@ -68,12 +67,14 @@ class LegacyApiPasswordAuthProvider(AuthProvider):
"""Validate password."""
api_password = str(self.config[CONF_API_PASSWORD])
if not hmac.compare_digest(api_password.encode('utf-8'),
password.encode('utf-8')):
if not hmac.compare_digest(
api_password.encode("utf-8"), password.encode("utf-8")
):
raise InvalidAuthError
async def async_get_or_create_credentials(
self, flow_result: Dict[str, str]) -> Credentials:
self, flow_result: Dict[str, str]
) -> Credentials:
"""Return credentials for this login."""
credentials = await self.async_credentials()
if credentials:
@ -82,7 +83,8 @@ class LegacyApiPasswordAuthProvider(AuthProvider):
return self.async_create_credentials({})
async def async_user_meta_for_credentials(
self, credentials: Credentials) -> UserMeta:
self, credentials: Credentials
) -> UserMeta:
"""
Return info for the user.
@ -95,23 +97,22 @@ class LegacyLoginFlow(LoginFlow):
"""Handler for the login flow."""
async def async_step_init(
self, user_input: Optional[Dict[str, str]] = None) \
-> Dict[str, Any]:
self, user_input: Optional[Dict[str, str]] = None
) -> Dict[str, Any]:
"""Handle the step of the form."""
errors = {}
if user_input is not None:
try:
cast(LegacyApiPasswordAuthProvider, self._auth_provider)\
.async_validate_login(user_input['password'])
cast(
LegacyApiPasswordAuthProvider, self._auth_provider
).async_validate_login(user_input["password"])
except InvalidAuthError:
errors['base'] = 'invalid_auth'
errors["base"] = "invalid_auth"
if not errors:
return await self.async_finish({})
return self.async_show_form(
step_id='init',
data_schema=vol.Schema({'password': str}),
errors=errors,
step_id="init", data_schema=vol.Schema({"password": str}), errors=errors
)

View file

@ -3,8 +3,7 @@
It shows list of users if access from trusted network.
Abort login flow if not access from trusted network.
"""
from ipaddress import ip_network, IPv4Address, IPv6Address, IPv4Network,\
IPv6Network
from ipaddress import ip_network, IPv4Address, IPv6Address, IPv4Network, IPv6Network
from typing import Any, Dict, List, Optional, Union, cast
import voluptuous as vol
@ -18,27 +17,32 @@ from ..models import Credentials, UserMeta
IPAddress = Union[IPv4Address, IPv6Address]
IPNetwork = Union[IPv4Network, IPv6Network]
CONF_TRUSTED_NETWORKS = 'trusted_networks'
CONF_TRUSTED_USERS = 'trusted_users'
CONF_GROUP = 'group'
CONF_ALLOW_BYPASS_LOGIN = 'allow_bypass_login'
CONF_TRUSTED_NETWORKS = "trusted_networks"
CONF_TRUSTED_USERS = "trusted_users"
CONF_GROUP = "group"
CONF_ALLOW_BYPASS_LOGIN = "allow_bypass_login"
CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend({
vol.Required(CONF_TRUSTED_NETWORKS): vol.All(
cv.ensure_list, [ip_network]
),
vol.Optional(CONF_TRUSTED_USERS, default={}): vol.Schema(
# we only validate the format of user_id or group_id
{ip_network: vol.All(
cv.ensure_list,
[vol.Or(
cv.uuid4_hex,
vol.Schema({vol.Required(CONF_GROUP): cv.uuid4_hex}),
)],
)}
),
vol.Optional(CONF_ALLOW_BYPASS_LOGIN, default=False): cv.boolean,
}, extra=vol.PREVENT_EXTRA)
CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend(
{
vol.Required(CONF_TRUSTED_NETWORKS): vol.All(cv.ensure_list, [ip_network]),
vol.Optional(CONF_TRUSTED_USERS, default={}): vol.Schema(
# we only validate the format of user_id or group_id
{
ip_network: vol.All(
cv.ensure_list,
[
vol.Or(
cv.uuid4_hex,
vol.Schema({vol.Required(CONF_GROUP): cv.uuid4_hex}),
)
],
)
}
),
vol.Optional(CONF_ALLOW_BYPASS_LOGIN, default=False): cv.boolean,
},
extra=vol.PREVENT_EXTRA,
)
class InvalidAuthError(HomeAssistantError):
@ -49,14 +53,14 @@ class InvalidUserError(HomeAssistantError):
"""Raised when try to login as invalid user."""
@AUTH_PROVIDERS.register('trusted_networks')
@AUTH_PROVIDERS.register("trusted_networks")
class TrustedNetworksAuthProvider(AuthProvider):
"""Trusted Networks auth provider.
Allow passwordless access from trusted network.
"""
DEFAULT_TITLE = 'Trusted Networks'
DEFAULT_TITLE = "Trusted Networks"
@property
def trusted_networks(self) -> List[IPNetwork]:
@ -76,49 +80,58 @@ class TrustedNetworksAuthProvider(AuthProvider):
async def async_login_flow(self, context: Optional[Dict]) -> LoginFlow:
"""Return a flow to login."""
assert context is not None
ip_addr = cast(IPAddress, context.get('ip_address'))
ip_addr = cast(IPAddress, context.get("ip_address"))
users = await self.store.async_get_users()
available_users = [user for user in users
if not user.system_generated and user.is_active]
available_users = [
user for user in users if not user.system_generated and user.is_active
]
for ip_net, user_or_group_list in self.trusted_users.items():
if ip_addr in ip_net:
user_list = [user_id for user_id in user_or_group_list
if isinstance(user_id, str)]
group_list = [group[CONF_GROUP] for group in user_or_group_list
if isinstance(group, dict)]
flattened_group_list = [group for sublist in group_list
for group in sublist]
user_list = [
user_id
for user_id in user_or_group_list
if isinstance(user_id, str)
]
group_list = [
group[CONF_GROUP]
for group in user_or_group_list
if isinstance(group, dict)
]
flattened_group_list = [
group for sublist in group_list for group in sublist
]
available_users = [
user for user in available_users
if (user.id in user_list or
any([group.id in flattened_group_list
for group in user.groups]))
user
for user in available_users
if (
user.id in user_list
or any(
[group.id in flattened_group_list for group in user.groups]
)
)
]
break
return TrustedNetworksLoginFlow(
self,
ip_addr,
{
user.id: user.name for user in available_users
},
{user.id: user.name for user in available_users},
self.config[CONF_ALLOW_BYPASS_LOGIN],
)
async def async_get_or_create_credentials(
self, flow_result: Dict[str, str]) -> Credentials:
self, flow_result: Dict[str, str]
) -> Credentials:
"""Get credentials based on the flow result."""
user_id = flow_result['user']
user_id = flow_result["user"]
users = await self.store.async_get_users()
for user in users:
if (not user.system_generated and
user.is_active and
user.id == user_id):
if not user.system_generated and user.is_active and user.id == user_id:
for credential in await self.async_credentials():
if credential.data['user_id'] == user_id:
if credential.data["user_id"] == user_id:
return credential
cred = self.async_create_credentials({'user_id': user_id})
cred = self.async_create_credentials({"user_id": user_id})
await self.store.async_link_user(user, cred)
return cred
@ -126,7 +139,8 @@ class TrustedNetworksAuthProvider(AuthProvider):
raise InvalidUserError
async def async_user_meta_for_credentials(
self, credentials: Credentials) -> UserMeta:
self, credentials: Credentials
) -> UserMeta:
"""Return extra user metadata for credentials.
Trusted network auth provider should never create new user.
@ -141,20 +155,24 @@ class TrustedNetworksAuthProvider(AuthProvider):
Raise InvalidAuthError if trusted_networks is not configured.
"""
if not self.trusted_networks:
raise InvalidAuthError('trusted_networks is not configured')
raise InvalidAuthError("trusted_networks is not configured")
if not any(ip_addr in trusted_network for trusted_network
in self.trusted_networks):
raise InvalidAuthError('Not in trusted_networks')
if not any(
ip_addr in trusted_network for trusted_network in self.trusted_networks
):
raise InvalidAuthError("Not in trusted_networks")
class TrustedNetworksLoginFlow(LoginFlow):
"""Handler for the login flow."""
def __init__(self, auth_provider: TrustedNetworksAuthProvider,
ip_addr: IPAddress,
available_users: Dict[str, Optional[str]],
allow_bypass_login: bool) -> None:
def __init__(
self,
auth_provider: TrustedNetworksAuthProvider,
ip_addr: IPAddress,
available_users: Dict[str, Optional[str]],
allow_bypass_login: bool,
) -> None:
"""Initialize the login flow."""
super().__init__(auth_provider)
self._available_users = available_users
@ -162,27 +180,26 @@ class TrustedNetworksLoginFlow(LoginFlow):
self._allow_bypass_login = allow_bypass_login
async def async_step_init(
self, user_input: Optional[Dict[str, str]] = None) \
-> Dict[str, Any]:
self, user_input: Optional[Dict[str, str]] = None
) -> Dict[str, Any]:
"""Handle the step of the form."""
try:
cast(TrustedNetworksAuthProvider, self._auth_provider)\
.async_validate_access(self._ip_address)
cast(
TrustedNetworksAuthProvider, self._auth_provider
).async_validate_access(self._ip_address)
except InvalidAuthError:
return self.async_abort(
reason='not_whitelisted'
)
return self.async_abort(reason="not_whitelisted")
if user_input is not None:
return await self.async_finish(user_input)
if self._allow_bypass_login and len(self._available_users) == 1:
return await self.async_finish({
'user': next(iter(self._available_users.keys()))
})
return await self.async_finish(
{"user": next(iter(self._available_users.keys()))}
)
return self.async_show_form(
step_id='init',
data_schema=vol.Schema({'user': vol.In(self._available_users)}),
step_id="init",
data_schema=vol.Schema({"user": vol.In(self._available_users)}),
)

View file

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

View file

@ -20,32 +20,33 @@ from homeassistant.exceptions import HomeAssistantError
_LOGGER = logging.getLogger(__name__)
ERROR_LOG_FILENAME = 'home-assistant.log'
ERROR_LOG_FILENAME = "home-assistant.log"
# hass.data key for logging information.
DATA_LOGGING = 'logging'
DATA_LOGGING = "logging"
DEBUGGER_INTEGRATIONS = {'ptvsd', }
CORE_INTEGRATIONS = ('homeassistant', 'persistent_notification')
LOGGING_INTEGRATIONS = {'logger', 'system_log'}
DEBUGGER_INTEGRATIONS = {"ptvsd"}
CORE_INTEGRATIONS = ("homeassistant", "persistent_notification")
LOGGING_INTEGRATIONS = {"logger", "system_log"}
STAGE_1_INTEGRATIONS = {
# To record data
'recorder',
"recorder",
# To make sure we forward data to other instances
'mqtt_eventstream',
"mqtt_eventstream",
}
async def async_from_config_dict(config: Dict[str, Any],
hass: core.HomeAssistant,
config_dir: Optional[str] = None,
enable_log: bool = True,
verbose: bool = False,
skip_pip: bool = False,
log_rotate_days: Any = None,
log_file: Any = None,
log_no_color: bool = False) \
-> Optional[core.HomeAssistant]:
async def async_from_config_dict(
config: Dict[str, Any],
hass: core.HomeAssistant,
config_dir: Optional[str] = None,
enable_log: bool = True,
verbose: bool = False,
skip_pip: bool = False,
log_rotate_days: Any = None,
log_file: Any = None,
log_no_color: bool = False,
) -> Optional[core.HomeAssistant]:
"""Try to configure Home Assistant from a configuration dictionary.
Dynamically loads required components and its dependencies.
@ -54,28 +55,30 @@ async def async_from_config_dict(config: Dict[str, Any],
start = time()
if enable_log:
async_enable_logging(hass, verbose, log_rotate_days, log_file,
log_no_color)
async_enable_logging(hass, verbose, log_rotate_days, log_file, log_no_color)
hass.config.skip_pip = skip_pip
if skip_pip:
_LOGGER.warning("Skipping pip installation of required modules. "
"This may cause issues")
_LOGGER.warning(
"Skipping pip installation of required modules. " "This may cause issues"
)
core_config = config.get(core.DOMAIN, {})
api_password = config.get('http', {}).get('api_password')
trusted_networks = config.get('http', {}).get('trusted_networks')
api_password = config.get("http", {}).get("api_password")
trusted_networks = config.get("http", {}).get("trusted_networks")
try:
await conf_util.async_process_ha_core_config(
hass, core_config, api_password, trusted_networks)
hass, core_config, api_password, trusted_networks
)
except vol.Invalid as config_err:
conf_util.async_log_exception(
config_err, 'homeassistant', core_config, hass)
conf_util.async_log_exception(config_err, "homeassistant", core_config, hass)
return None
except HomeAssistantError:
_LOGGER.error("Home Assistant core failed to initialize. "
"Further initialization aborted")
_LOGGER.error(
"Home Assistant core failed to initialize. "
"Further initialization aborted"
)
return None
# Make a copy because we are mutating it.
@ -83,7 +86,8 @@ async def async_from_config_dict(config: Dict[str, Any],
# Merge packages
await conf_util.merge_packages_config(
hass, config, core_config.get(conf_util.CONF_PACKAGES, {}))
hass, config, core_config.get(conf_util.CONF_PACKAGES, {})
)
hass.config_entries = config_entries.ConfigEntries(hass, config)
await hass.config_entries.async_initialize()
@ -91,19 +95,20 @@ async def async_from_config_dict(config: Dict[str, Any],
await _async_set_up_integrations(hass, config)
stop = time()
_LOGGER.info("Home Assistant initialized in %.2fs", stop-start)
_LOGGER.info("Home Assistant initialized in %.2fs", stop - start)
return hass
async def async_from_config_file(config_path: str,
hass: core.HomeAssistant,
verbose: bool = False,
skip_pip: bool = True,
log_rotate_days: Any = None,
log_file: Any = None,
log_no_color: bool = False)\
-> Optional[core.HomeAssistant]:
async def async_from_config_file(
config_path: str,
hass: core.HomeAssistant,
verbose: bool = False,
skip_pip: bool = True,
log_rotate_days: Any = None,
log_file: Any = None,
log_no_color: bool = False,
) -> Optional[core.HomeAssistant]:
"""Read the configuration file and try to start all the functionality.
Will add functionality to 'hass' parameter.
@ -116,15 +121,14 @@ async def async_from_config_file(config_path: str,
if not is_virtual_env():
await async_mount_local_lib_path(config_dir)
async_enable_logging(hass, verbose, log_rotate_days, log_file,
log_no_color)
async_enable_logging(hass, verbose, log_rotate_days, log_file, log_no_color)
await hass.async_add_executor_job(
conf_util.process_ha_config_upgrade, hass)
await hass.async_add_executor_job(conf_util.process_ha_config_upgrade, hass)
try:
config_dict = await hass.async_add_executor_job(
conf_util.load_yaml_config_file, config_path)
conf_util.load_yaml_config_file, config_path
)
except HomeAssistantError as err:
_LOGGER.error("Error loading %s: %s", config_path, err)
return None
@ -132,43 +136,48 @@ async def async_from_config_file(config_path: str,
clear_secret_cache()
return await async_from_config_dict(
config_dict, hass, enable_log=False, skip_pip=skip_pip)
config_dict, hass, enable_log=False, skip_pip=skip_pip
)
@core.callback
def async_enable_logging(hass: core.HomeAssistant,
verbose: bool = False,
log_rotate_days: Optional[int] = None,
log_file: Optional[str] = None,
log_no_color: bool = False) -> None:
def async_enable_logging(
hass: core.HomeAssistant,
verbose: bool = False,
log_rotate_days: Optional[int] = None,
log_file: Optional[str] = None,
log_no_color: bool = False,
) -> None:
"""Set up the logging.
This method must be run in the event loop.
"""
fmt = ("%(asctime)s %(levelname)s (%(threadName)s) "
"[%(name)s] %(message)s")
datefmt = '%Y-%m-%d %H:%M:%S'
fmt = "%(asctime)s %(levelname)s (%(threadName)s) " "[%(name)s] %(message)s"
datefmt = "%Y-%m-%d %H:%M:%S"
if not log_no_color:
try:
from colorlog import ColoredFormatter
# basicConfig must be called after importing colorlog in order to
# ensure that the handlers it sets up wraps the correct streams.
logging.basicConfig(level=logging.INFO)
colorfmt = "%(log_color)s{}%(reset)s".format(fmt)
logging.getLogger().handlers[0].setFormatter(ColoredFormatter(
colorfmt,
datefmt=datefmt,
reset=True,
log_colors={
'DEBUG': 'cyan',
'INFO': 'green',
'WARNING': 'yellow',
'ERROR': 'red',
'CRITICAL': 'red',
}
))
logging.getLogger().handlers[0].setFormatter(
ColoredFormatter(
colorfmt,
datefmt=datefmt,
reset=True,
log_colors={
"DEBUG": "cyan",
"INFO": "green",
"WARNING": "yellow",
"ERROR": "red",
"CRITICAL": "red",
},
)
)
except ImportError:
pass
@ -177,9 +186,9 @@ def async_enable_logging(hass: core.HomeAssistant,
logging.basicConfig(format=fmt, datefmt=datefmt, level=logging.INFO)
# Suppress overly verbose logs from libraries that aren't helpful
logging.getLogger('requests').setLevel(logging.WARNING)
logging.getLogger('urllib3').setLevel(logging.WARNING)
logging.getLogger('aiohttp.access').setLevel(logging.WARNING)
logging.getLogger("requests").setLevel(logging.WARNING)
logging.getLogger("urllib3").setLevel(logging.WARNING)
logging.getLogger("aiohttp.access").setLevel(logging.WARNING)
# Log errors to a file if we have write access to file or config dir
if log_file is None:
@ -192,16 +201,16 @@ def async_enable_logging(hass: core.HomeAssistant,
# Check if we can write to the error log if it exists or that
# we can create files in the containing directory if not.
if (err_path_exists and os.access(err_log_path, os.W_OK)) or \
(not err_path_exists and os.access(err_dir, os.W_OK)):
if (err_path_exists and os.access(err_log_path, os.W_OK)) or (
not err_path_exists and os.access(err_dir, os.W_OK)
):
if log_rotate_days:
err_handler = logging.handlers.TimedRotatingFileHandler(
err_log_path, when='midnight',
backupCount=log_rotate_days) # type: logging.FileHandler
err_log_path, when="midnight", backupCount=log_rotate_days
) # type: logging.FileHandler
else:
err_handler = logging.FileHandler(
err_log_path, mode='w', delay=True)
err_handler = logging.FileHandler(err_log_path, mode="w", delay=True)
err_handler.setLevel(logging.INFO if verbose else logging.WARNING)
err_handler.setFormatter(logging.Formatter(fmt, datefmt=datefmt))
@ -210,21 +219,19 @@ def async_enable_logging(hass: core.HomeAssistant,
async def async_stop_async_handler(_: Any) -> None:
"""Cleanup async handler."""
logging.getLogger('').removeHandler(async_handler) # type: ignore
logging.getLogger("").removeHandler(async_handler) # type: ignore
await async_handler.async_close(blocking=True)
hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_CLOSE, async_stop_async_handler)
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_CLOSE, async_stop_async_handler)
logger = logging.getLogger('')
logger = logging.getLogger("")
logger.addHandler(async_handler) # type: ignore
logger.setLevel(logging.INFO)
# Save the log file location for access by other components.
hass.data[DATA_LOGGING] = err_log_path
else:
_LOGGER.error(
"Unable to set up error log %s (access denied)", err_log_path)
_LOGGER.error("Unable to set up error log %s (access denied)", err_log_path)
async def async_mount_local_lib_path(config_dir: str) -> str:
@ -232,7 +239,7 @@ async def async_mount_local_lib_path(config_dir: str) -> str:
This function is a coroutine.
"""
deps_dir = os.path.join(config_dir, 'deps')
deps_dir = os.path.join(config_dir, "deps")
lib_dir = await async_get_user_site(deps_dir)
if lib_dir not in sys.path:
sys.path.insert(0, lib_dir)
@ -243,21 +250,21 @@ async def async_mount_local_lib_path(config_dir: str) -> str:
def _get_domains(hass: core.HomeAssistant, config: Dict[str, Any]) -> Set[str]:
"""Get domains of components to set up."""
# Filter out the repeating and common config section [homeassistant]
domains = set(key.split(' ')[0] for key in config.keys()
if key != core.DOMAIN)
domains = set(key.split(" ")[0] for key in config.keys() if key != core.DOMAIN)
# Add config entry domains
domains.update(hass.config_entries.async_domains()) # type: ignore
# Make sure the Hass.io component is loaded
if 'HASSIO' in os.environ:
domains.add('hassio')
if "HASSIO" in os.environ:
domains.add("hassio")
return domains
async def _async_set_up_integrations(
hass: core.HomeAssistant, config: Dict[str, Any]) -> None:
hass: core.HomeAssistant, config: Dict[str, Any]
) -> None:
"""Set up all the integrations."""
domains = _get_domains(hass, config)
@ -265,27 +272,33 @@ async def _async_set_up_integrations(
debuggers = domains & DEBUGGER_INTEGRATIONS
if debuggers:
_LOGGER.debug("Starting up debuggers %s", debuggers)
await asyncio.gather(*(
async_setup_component(hass, domain, config)
for domain in debuggers))
await asyncio.gather(
*(async_setup_component(hass, domain, config) for domain in debuggers)
)
domains -= DEBUGGER_INTEGRATIONS
# Resolve all dependencies of all components so we can find the logging
# and integrations that need faster initialization.
resolved_domains_task = asyncio.gather(*(
loader.async_component_dependencies(hass, domain)
for domain in domains
), return_exceptions=True)
resolved_domains_task = asyncio.gather(
*(loader.async_component_dependencies(hass, domain) for domain in domains),
return_exceptions=True,
)
# Set up core.
_LOGGER.debug("Setting up %s", CORE_INTEGRATIONS)
if not all(await asyncio.gather(*(
async_setup_component(hass, domain, config)
for domain in CORE_INTEGRATIONS
))):
_LOGGER.error("Home Assistant core failed to initialize. "
"Further initialization aborted")
if not all(
await asyncio.gather(
*(
async_setup_component(hass, domain, config)
for domain in CORE_INTEGRATIONS
)
)
):
_LOGGER.error(
"Home Assistant core failed to initialize. "
"Further initialization aborted"
)
return
_LOGGER.debug("Home Assistant core initialized")
@ -305,36 +318,32 @@ async def _async_set_up_integrations(
if logging_domains:
_LOGGER.info("Setting up %s", logging_domains)
await asyncio.gather(*(
async_setup_component(hass, domain, config)
for domain in logging_domains
))
await asyncio.gather(
*(async_setup_component(hass, domain, config) for domain in logging_domains)
)
# Kick off loading the registries. They don't need to be awaited.
asyncio.gather(
hass.helpers.device_registry.async_get_registry(),
hass.helpers.entity_registry.async_get_registry(),
hass.helpers.area_registry.async_get_registry())
hass.helpers.area_registry.async_get_registry(),
)
if stage_1_domains:
await asyncio.gather(*(
async_setup_component(hass, domain, config)
for domain in stage_1_domains
))
await asyncio.gather(
*(async_setup_component(hass, domain, config) for domain in stage_1_domains)
)
# Load all integrations
after_dependencies = {} # type: Dict[str, Set[str]]
for int_or_exc in await asyncio.gather(*(
loader.async_get_integration(hass, domain)
for domain in stage_2_domains
), return_exceptions=True):
for int_or_exc in await asyncio.gather(
*(loader.async_get_integration(hass, domain) for domain in stage_2_domains),
return_exceptions=True,
):
# Exceptions are handled in async_setup_component.
if (isinstance(int_or_exc, loader.Integration) and
int_or_exc.after_dependencies):
after_dependencies[int_or_exc.domain] = set(
int_or_exc.after_dependencies
)
if isinstance(int_or_exc, loader.Integration) and int_or_exc.after_dependencies:
after_dependencies[int_or_exc.domain] = set(int_or_exc.after_dependencies)
last_load = None
while stage_2_domains:
@ -344,8 +353,7 @@ async def _async_set_up_integrations(
after_deps = after_dependencies.get(domain)
# Load if integration has no after_dependencies or they are
# all loaded
if (not after_deps or
not after_deps-hass.config.components):
if not after_deps or not after_deps - hass.config.components:
domains_to_load.add(domain)
if not domains_to_load or domains_to_load == last_load:
@ -353,10 +361,9 @@ async def _async_set_up_integrations(
_LOGGER.debug("Setting up %s", domains_to_load)
await asyncio.gather(*(
async_setup_component(hass, domain, config)
for domain in domains_to_load
))
await asyncio.gather(
*(async_setup_component(hass, domain, config) for domain in domains_to_load)
)
last_load = domains_to_load
stage_2_domains -= domains_to_load
@ -366,10 +373,9 @@ async def _async_set_up_integrations(
if stage_2_domains:
_LOGGER.debug("Final set up: %s", stage_2_domains)
await asyncio.gather(*(
async_setup_component(hass, domain, config)
for domain in stage_2_domains
))
await asyncio.gather(
*(async_setup_component(hass, domain, config) for domain in stage_2_domains)
)
# Wrap up startup
await hass.async_block_till_done()

View file

@ -31,11 +31,10 @@ def is_on(hass, entity_id=None):
component = getattr(hass.components, domain)
except ImportError:
_LOGGER.error('Failed to call %s.is_on: component not found',
domain)
_LOGGER.error("Failed to call %s.is_on: component not found", domain)
continue
if not hasattr(component, 'is_on'):
if not hasattr(component, "is_on"):
_LOGGER.warning("Integration %s has no is_on method.", domain)
continue

View file

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

View file

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

View file

@ -15,9 +15,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
data = hass.data[ABODE_DOMAIN]
device_types = [CONST.TYPE_CONNECTIVITY, CONST.TYPE_MOISTURE,
CONST.TYPE_MOTION, CONST.TYPE_OCCUPANCY,
CONST.TYPE_OPENING]
device_types = [
CONST.TYPE_CONNECTIVITY,
CONST.TYPE_MOISTURE,
CONST.TYPE_MOTION,
CONST.TYPE_OCCUPANCY,
CONST.TYPE_OPENING,
]
devices = []
for device in data.abode.get_devices(generic_type=device_types):
@ -26,13 +30,15 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
devices.append(AbodeBinarySensor(data, device))
for automation in data.abode.get_automations(
generic_type=CONST.TYPE_QUICK_ACTION):
for automation in data.abode.get_automations(generic_type=CONST.TYPE_QUICK_ACTION):
if data.is_automation_excluded(automation):
continue
devices.append(AbodeQuickActionBinarySensor(
data, automation, TIMELINE.AUTOMATION_EDIT_GROUP))
devices.append(
AbodeQuickActionBinarySensor(
data, automation, TIMELINE.AUTOMATION_EDIT_GROUP
)
)
data.devices.extend(devices)

View file

@ -49,7 +49,8 @@ class AbodeCamera(AbodeDevice, Camera):
self.hass.async_add_job(
self._data.abode.events.add_timeline_callback,
self._event, self._capture_callback
self._event,
self._capture_callback,
)
def capture(self):
@ -66,8 +67,7 @@ class AbodeCamera(AbodeDevice, Camera):
"""Attempt to download the most recent capture."""
if self._device.image_url:
try:
self._response = requests.get(
self._device.image_url, stream=True)
self._response = requests.get(self._device.image_url, stream=True)
self._response.raise_for_status()
except requests.HTTPError as err:

View file

@ -3,10 +3,18 @@ import logging
from math import ceil
from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS,
SUPPORT_COLOR, SUPPORT_COLOR_TEMP, Light)
ATTR_BRIGHTNESS,
ATTR_COLOR_TEMP,
ATTR_HS_COLOR,
SUPPORT_BRIGHTNESS,
SUPPORT_COLOR,
SUPPORT_COLOR_TEMP,
Light,
)
from homeassistant.util.color import (
color_temperature_kelvin_to_mired, color_temperature_mired_to_kelvin)
color_temperature_kelvin_to_mired,
color_temperature_mired_to_kelvin,
)
from . import DOMAIN as ABODE_DOMAIN, AbodeDevice
@ -42,8 +50,8 @@ class AbodeLight(AbodeDevice, Light):
"""Turn on the light."""
if ATTR_COLOR_TEMP in kwargs and self._device.is_color_capable:
self._device.set_color_temp(
int(color_temperature_mired_to_kelvin(
kwargs[ATTR_COLOR_TEMP])))
int(color_temperature_mired_to_kelvin(kwargs[ATTR_COLOR_TEMP]))
)
if ATTR_HS_COLOR in kwargs and self._device.is_color_capable:
self._device.set_color(kwargs[ATTR_HS_COLOR])

View file

@ -2,7 +2,10 @@
import logging
from homeassistant.const import (
DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE)
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_ILLUMINANCE,
DEVICE_CLASS_TEMPERATURE,
)
from . import DOMAIN as ABODE_DOMAIN, AbodeDevice
@ -10,9 +13,9 @@ _LOGGER = logging.getLogger(__name__)
# Sensor types: Name, icon
SENSOR_TYPES = {
'temp': ['Temperature', DEVICE_CLASS_TEMPERATURE],
'humidity': ['Humidity', DEVICE_CLASS_HUMIDITY],
'lux': ['Lux', DEVICE_CLASS_ILLUMINANCE],
"temp": ["Temperature", DEVICE_CLASS_TEMPERATURE],
"humidity": ["Humidity", DEVICE_CLASS_HUMIDITY],
"lux": ["Lux", DEVICE_CLASS_ILLUMINANCE],
}
@ -42,8 +45,9 @@ class AbodeSensor(AbodeDevice):
"""Initialize a sensor for an Abode device."""
super().__init__(data, device)
self._sensor_type = sensor_type
self._name = '{0} {1}'.format(
self._device.name, SENSOR_TYPES[self._sensor_type][0])
self._name = "{0} {1}".format(
self._device.name, SENSOR_TYPES[self._sensor_type][0]
)
self._device_class = SENSOR_TYPES[self._sensor_type][1]
@property
@ -59,19 +63,19 @@ class AbodeSensor(AbodeDevice):
@property
def state(self):
"""Return the state of the sensor."""
if self._sensor_type == 'temp':
if self._sensor_type == "temp":
return self._device.temp
if self._sensor_type == 'humidity':
if self._sensor_type == "humidity":
return self._device.humidity
if self._sensor_type == 'lux':
if self._sensor_type == "lux":
return self._device.lux
@property
def unit_of_measurement(self):
"""Return the units of measurement."""
if self._sensor_type == 'temp':
if self._sensor_type == "temp":
return self._device.temp_unit
if self._sensor_type == 'humidity':
if self._sensor_type == "humidity":
return self._device.humidity_unit
if self._sensor_type == 'lux':
if self._sensor_type == "lux":
return self._device.lux_unit

View file

@ -25,13 +25,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
devices.append(AbodeSwitch(data, device))
# Get all Abode automations that can be enabled/disabled
for automation in data.abode.get_automations(
generic_type=CONST.TYPE_AUTOMATION):
for automation in data.abode.get_automations(generic_type=CONST.TYPE_AUTOMATION):
if data.is_automation_excluded(automation):
continue
devices.append(AbodeAutomationSwitch(
data, automation, TIMELINE.AUTOMATION_EDIT_GROUP))
devices.append(
AbodeAutomationSwitch(data, automation, TIMELINE.AUTOMATION_EDIT_GROUP)
)
data.devices.extend(devices)

View file

@ -4,50 +4,58 @@ import re
import voluptuous as vol
from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA)
from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA
from homeassistant.const import (
STATE_ON, STATE_OFF, STATE_UNKNOWN, CONF_NAME, CONF_FILENAME)
STATE_ON,
STATE_OFF,
STATE_UNKNOWN,
CONF_NAME,
CONF_FILENAME,
)
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
CONF_TIMEOUT = 'timeout'
CONF_WRITE_TIMEOUT = 'write_timeout'
CONF_TIMEOUT = "timeout"
CONF_WRITE_TIMEOUT = "write_timeout"
DEFAULT_NAME = 'Acer Projector'
DEFAULT_NAME = "Acer Projector"
DEFAULT_TIMEOUT = 1
DEFAULT_WRITE_TIMEOUT = 1
ECO_MODE = 'ECO Mode'
ECO_MODE = "ECO Mode"
ICON = 'mdi:projector'
ICON = "mdi:projector"
INPUT_SOURCE = 'Input Source'
INPUT_SOURCE = "Input Source"
LAMP = 'Lamp'
LAMP_HOURS = 'Lamp Hours'
LAMP = "Lamp"
LAMP_HOURS = "Lamp Hours"
MODEL = 'Model'
MODEL = "Model"
# Commands known to the projector
CMD_DICT = {
LAMP: '* 0 Lamp ?\r',
LAMP_HOURS: '* 0 Lamp\r',
INPUT_SOURCE: '* 0 Src ?\r',
ECO_MODE: '* 0 IR 052\r',
MODEL: '* 0 IR 035\r',
STATE_ON: '* 0 IR 001\r',
STATE_OFF: '* 0 IR 002\r',
LAMP: "* 0 Lamp ?\r",
LAMP_HOURS: "* 0 Lamp\r",
INPUT_SOURCE: "* 0 Src ?\r",
ECO_MODE: "* 0 IR 052\r",
MODEL: "* 0 IR 035\r",
STATE_ON: "* 0 IR 001\r",
STATE_OFF: "* 0 IR 002\r",
}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_FILENAME): cv.isdevice,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
vol.Optional(CONF_WRITE_TIMEOUT, default=DEFAULT_WRITE_TIMEOUT):
cv.positive_int,
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_FILENAME): cv.isdevice,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
vol.Optional(
CONF_WRITE_TIMEOUT, default=DEFAULT_WRITE_TIMEOUT
): cv.positive_int,
}
)
def setup_platform(hass, config, add_entities, discovery_info=None):
@ -66,9 +74,10 @@ class AcerSwitch(SwitchDevice):
def __init__(self, serial_port, name, timeout, write_timeout, **kwargs):
"""Init of the Acer projector."""
import serial
self.ser = serial.Serial(
port=serial_port, timeout=timeout, write_timeout=write_timeout,
**kwargs)
port=serial_port, timeout=timeout, write_timeout=write_timeout, **kwargs
)
self._serial_port = serial_port
self._name = name
self._state = False
@ -82,6 +91,7 @@ class AcerSwitch(SwitchDevice):
def _write_read(self, msg):
"""Write to the projector and read the return."""
import serial
ret = ""
# Sometimes the projector won't answer for no reason or the projector
# was disconnected during runtime.
@ -89,14 +99,14 @@ class AcerSwitch(SwitchDevice):
try:
if not self.ser.is_open:
self.ser.open()
msg = msg.encode('utf-8')
msg = msg.encode("utf-8")
self.ser.write(msg)
# Size is an experience value there is no real limit.
# AFAIK there is no limit and no end character so we will usually
# need to wait for timeout
ret = self.ser.read_until(size=20).decode('utf-8')
ret = self.ser.read_until(size=20).decode("utf-8")
except serial.SerialException:
_LOGGER.error('Problem communicating with %s', self._serial_port)
_LOGGER.error("Problem communicating with %s", self._serial_port)
self.ser.close()
return ret
@ -104,7 +114,7 @@ class AcerSwitch(SwitchDevice):
"""Write msg, obtain answer and format output."""
# answers are formatted as ***\answer\r***
awns = self._write_read(msg)
match = re.search(r'\r(.+)\r', awns)
match = re.search(r"\r(.+)\r", awns)
if match:
return match.group(1)
return STATE_UNKNOWN
@ -133,10 +143,10 @@ class AcerSwitch(SwitchDevice):
"""Get the latest state from the projector."""
msg = CMD_DICT[LAMP]
awns = self._write_read_format(msg)
if awns == 'Lamp 1':
if awns == "Lamp 1":
self._state = True
self._available = True
elif awns == 'Lamp 0':
elif awns == "Lamp 0":
self._state = False
self._available = True
else:

View file

@ -8,22 +8,28 @@ import voluptuous as vol
import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util
from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
DOMAIN,
PLATFORM_SCHEMA,
DeviceScanner,
)
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
_LOGGER = logging.getLogger(__name__)
_LEASES_REGEX = re.compile(
r'(?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})' +
r'\smac:\s(?P<mac>([0-9a-f]{2}[:-]){5}([0-9a-f]{2}))' +
r'\svalid\sfor:\s(?P<timevalid>(-?\d+))' +
r'\ssec')
r"(?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})"
+ r"\smac:\s(?P<mac>([0-9a-f]{2}[:-]){5}([0-9a-f]{2}))"
+ r"\svalid\sfor:\s(?P<timevalid>(-?\d+))"
+ r"\ssec"
)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_USERNAME): cv.string
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_USERNAME): cv.string,
}
)
def get_scanner(hass, config):
@ -32,7 +38,7 @@ def get_scanner(hass, config):
return scanner if scanner.success_init else None
Device = namedtuple('Device', ['mac', 'ip', 'last_update'])
Device = namedtuple("Device", ["mac", "ip", "last_update"])
class ActiontecDeviceScanner(DeviceScanner):
@ -75,9 +81,11 @@ class ActiontecDeviceScanner(DeviceScanner):
actiontec_data = self.get_actiontec_data()
if not actiontec_data:
return False
self.last_results = [Device(data['mac'], name, now)
for name, data in actiontec_data.items()
if data['timevalid'] > -60]
self.last_results = [
Device(data["mac"], name, now)
for name, data in actiontec_data.items()
if data["timevalid"] > -60
]
_LOGGER.info("Scan successful")
return True
@ -85,17 +93,16 @@ class ActiontecDeviceScanner(DeviceScanner):
"""Retrieve data from Actiontec MI424WR and return parsed result."""
try:
telnet = telnetlib.Telnet(self.host)
telnet.read_until(b'Username: ')
telnet.write((self.username + '\n').encode('ascii'))
telnet.read_until(b'Password: ')
telnet.write((self.password + '\n').encode('ascii'))
prompt = telnet.read_until(
b'Wireless Broadband Router> ').split(b'\n')[-1]
telnet.write('firewall mac_cache_dump\n'.encode('ascii'))
telnet.write('\n'.encode('ascii'))
telnet.read_until(b"Username: ")
telnet.write((self.username + "\n").encode("ascii"))
telnet.read_until(b"Password: ")
telnet.write((self.password + "\n").encode("ascii"))
prompt = telnet.read_until(b"Wireless Broadband Router> ").split(b"\n")[-1]
telnet.write("firewall mac_cache_dump\n".encode("ascii"))
telnet.write("\n".encode("ascii"))
telnet.read_until(prompt)
leases_result = telnet.read_until(prompt).split(b'\n')[1:-1]
telnet.write('exit\n'.encode('ascii'))
leases_result = telnet.read_until(prompt).split(b"\n")[1:-1]
telnet.write("exit\n".encode("ascii"))
except EOFError:
_LOGGER.exception("Unexpected response from router")
return
@ -105,11 +112,11 @@ class ActiontecDeviceScanner(DeviceScanner):
devices = {}
for lease in leases_result:
match = _LEASES_REGEX.search(lease.decode('utf-8'))
match = _LEASES_REGEX.search(lease.decode("utf-8"))
if match is not None:
devices[match.group('ip')] = {
'ip': match.group('ip'),
'mac': match.group('mac').upper(),
'timevalid': int(match.group('timevalid'))
}
devices[match.group("ip")] = {
"ip": match.group("ip"),
"mac": match.group("mac").upper(),
"timevalid": int(match.group("timevalid")),
}
return devices

View file

@ -6,13 +6,27 @@ from adguardhome import AdGuardHome, AdGuardHomeError
import voluptuous as vol
from homeassistant.components.adguard.const import (
CONF_FORCE, DATA_ADGUARD_CLIENT, DATA_ADGUARD_VERION, DOMAIN,
SERVICE_ADD_URL, SERVICE_DISABLE_URL, SERVICE_ENABLE_URL, SERVICE_REFRESH,
SERVICE_REMOVE_URL)
CONF_FORCE,
DATA_ADGUARD_CLIENT,
DATA_ADGUARD_VERION,
DOMAIN,
SERVICE_ADD_URL,
SERVICE_DISABLE_URL,
SERVICE_ENABLE_URL,
SERVICE_REFRESH,
SERVICE_REMOVE_URL,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_SSL, CONF_URL,
CONF_USERNAME, CONF_VERIFY_SSL)
CONF_HOST,
CONF_NAME,
CONF_PASSWORD,
CONF_PORT,
CONF_SSL,
CONF_URL,
CONF_USERNAME,
CONF_VERIFY_SSL,
)
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.entity import Entity
@ -34,9 +48,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
return True
async def async_setup_entry(
hass: HomeAssistantType, entry: ConfigEntry
) -> bool:
async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool:
"""Set up AdGuard Home from a config entry."""
session = async_get_clientsession(hass, entry.data[CONF_VERIFY_SSL])
adguard = AdGuardHome(
@ -52,7 +64,7 @@ async def async_setup_entry(
hass.data.setdefault(DOMAIN, {})[DATA_ADGUARD_CLIENT] = adguard
for component in 'sensor', 'switch':
for component in "sensor", "switch":
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, component)
)
@ -98,9 +110,7 @@ async def async_setup_entry(
return True
async def async_unload_entry(
hass: HomeAssistantType, entry: ConfigType
) -> bool:
async def async_unload_entry(hass: HomeAssistantType, entry: ConfigType) -> bool:
"""Unload AdGuard Home config entry."""
hass.services.async_remove(DOMAIN, SERVICE_ADD_URL)
hass.services.async_remove(DOMAIN, SERVICE_REMOVE_URL)
@ -108,7 +118,7 @@ async def async_unload_entry(
hass.services.async_remove(DOMAIN, SERVICE_DISABLE_URL)
hass.services.async_remove(DOMAIN, SERVICE_REFRESH)
for component in 'sensor', 'switch':
for component in "sensor", "switch":
await hass.config_entries.async_forward_entry_unload(entry, component)
del hass.data[DOMAIN]
@ -166,15 +176,10 @@ class AdGuardHomeDeviceEntity(AdGuardHomeEntity):
def device_info(self) -> Dict[str, Any]:
"""Return device information about this AdGuard Home instance."""
return {
'identifiers': {
(
DOMAIN,
self.adguard.host,
self.adguard.port,
self.adguard.base_path,
)
"identifiers": {
(DOMAIN, self.adguard.host, self.adguard.port, self.adguard.base_path)
},
'name': 'AdGuard Home',
'manufacturer': 'AdGuard Team',
'sw_version': self.hass.data[DOMAIN].get(DATA_ADGUARD_VERION),
"name": "AdGuard Home",
"manufacturer": "AdGuard Team",
"sw_version": self.hass.data[DOMAIN].get(DATA_ADGUARD_VERION),
}

View file

@ -8,8 +8,13 @@ from homeassistant import config_entries
from homeassistant.components.adguard.const import DOMAIN
from homeassistant.config_entries import ConfigFlow
from homeassistant.const import (
CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_SSL, CONF_USERNAME,
CONF_VERIFY_SSL)
CONF_HOST,
CONF_PASSWORD,
CONF_PORT,
CONF_SSL,
CONF_USERNAME,
CONF_VERIFY_SSL,
)
from homeassistant.helpers.aiohttp_client import async_get_clientsession
_LOGGER = logging.getLogger(__name__)
@ -31,7 +36,7 @@ class AdGuardHomeFlowHandler(ConfigFlow):
async def _show_setup_form(self, errors=None):
"""Show the setup form to the user."""
return self.async_show_form(
step_id='user',
step_id="user",
data_schema=vol.Schema(
{
vol.Required(CONF_HOST): str,
@ -48,10 +53,8 @@ class AdGuardHomeFlowHandler(ConfigFlow):
async def _show_hassio_form(self, errors=None):
"""Show the Hass.io confirmation form to the user."""
return self.async_show_form(
step_id='hassio_confirm',
description_placeholders={
'addon': self._hassio_discovery['addon']
},
step_id="hassio_confirm",
description_placeholders={"addon": self._hassio_discovery["addon"]},
data_schema=vol.Schema({}),
errors=errors or {},
)
@ -59,16 +62,14 @@ class AdGuardHomeFlowHandler(ConfigFlow):
async def async_step_user(self, user_input=None):
"""Handle a flow initiated by the user."""
if self._async_current_entries():
return self.async_abort(reason='single_instance_allowed')
return self.async_abort(reason="single_instance_allowed")
if user_input is None:
return await self._show_setup_form(user_input)
errors = {}
session = async_get_clientsession(
self.hass, user_input[CONF_VERIFY_SSL]
)
session = async_get_clientsession(self.hass, user_input[CONF_VERIFY_SSL])
adguard = AdGuardHome(
user_input[CONF_HOST],
@ -84,7 +85,7 @@ class AdGuardHomeFlowHandler(ConfigFlow):
try:
await adguard.version()
except AdGuardHomeConnectionError:
errors['base'] = 'connection_error'
errors["base"] = "connection_error"
return await self._show_setup_form(errors)
return self.async_create_entry(
@ -112,25 +113,30 @@ class AdGuardHomeFlowHandler(ConfigFlow):
cur_entry = entries[0]
if (cur_entry.data[CONF_HOST] == user_input[CONF_HOST] and
cur_entry.data[CONF_PORT] == user_input[CONF_PORT]):
return self.async_abort(reason='single_instance_allowed')
if (
cur_entry.data[CONF_HOST] == user_input[CONF_HOST]
and cur_entry.data[CONF_PORT] == user_input[CONF_PORT]
):
return self.async_abort(reason="single_instance_allowed")
is_loaded = cur_entry.state == config_entries.ENTRY_STATE_LOADED
if is_loaded:
await self.hass.config_entries.async_unload(cur_entry.entry_id)
self.hass.config_entries.async_update_entry(cur_entry, data={
**cur_entry.data,
CONF_HOST: user_input[CONF_HOST],
CONF_PORT: user_input[CONF_PORT],
})
self.hass.config_entries.async_update_entry(
cur_entry,
data={
**cur_entry.data,
CONF_HOST: user_input[CONF_HOST],
CONF_PORT: user_input[CONF_PORT],
},
)
if is_loaded:
await self.hass.config_entries.async_setup(cur_entry.entry_id)
return self.async_abort(reason='existing_instance_updated')
return self.async_abort(reason="existing_instance_updated")
async def async_step_hassio_confirm(self, user_input=None):
"""Confirm Hass.io discovery."""
@ -152,11 +158,11 @@ class AdGuardHomeFlowHandler(ConfigFlow):
try:
await adguard.version()
except AdGuardHomeConnectionError:
errors['base'] = 'connection_error'
errors["base"] = "connection_error"
return await self._show_hassio_form(errors)
return self.async_create_entry(
title=self._hassio_discovery['addon'],
title=self._hassio_discovery["addon"],
data={
CONF_HOST: self._hassio_discovery[CONF_HOST],
CONF_PORT: self._hassio_discovery[CONF_PORT],

View file

@ -1,14 +1,14 @@
"""Constants for the AdGuard Home integration."""
DOMAIN = 'adguard'
DOMAIN = "adguard"
DATA_ADGUARD_CLIENT = 'adguard_client'
DATA_ADGUARD_VERION = 'adguard_version'
DATA_ADGUARD_CLIENT = "adguard_client"
DATA_ADGUARD_VERION = "adguard_version"
CONF_FORCE = 'force'
CONF_FORCE = "force"
SERVICE_ADD_URL = 'add_url'
SERVICE_DISABLE_URL = 'disable_url'
SERVICE_ENABLE_URL = 'enable_url'
SERVICE_REFRESH = 'refresh'
SERVICE_REMOVE_URL = 'remove_url'
SERVICE_ADD_URL = "add_url"
SERVICE_DISABLE_URL = "disable_url"
SERVICE_ENABLE_URL = "enable_url"
SERVICE_REFRESH = "refresh"
SERVICE_REMOVE_URL = "remove_url"

View file

@ -6,7 +6,10 @@ from adguardhome import AdGuardHomeConnectionError
from homeassistant.components.adguard import AdGuardHomeDeviceEntity
from homeassistant.components.adguard.const import (
DATA_ADGUARD_CLIENT, DATA_ADGUARD_VERION, DOMAIN)
DATA_ADGUARD_CLIENT,
DATA_ADGUARD_VERION,
DOMAIN,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers.typing import HomeAssistantType
@ -18,7 +21,7 @@ PARALLEL_UPDATES = 4
async def async_setup_entry(
hass: HomeAssistantType, entry: ConfigEntry, async_add_entities
hass: HomeAssistantType, entry: ConfigEntry, async_add_entities
) -> None:
"""Set up AdGuard Home sensor based on a config entry."""
adguard = hass.data[DOMAIN][DATA_ADGUARD_CLIENT]
@ -48,12 +51,7 @@ class AdGuardHomeSensor(AdGuardHomeDeviceEntity):
"""Defines a AdGuard Home sensor."""
def __init__(
self,
adguard,
name: str,
icon: str,
measurement: str,
unit_of_measurement: str,
self, adguard, name: str, icon: str, measurement: str, unit_of_measurement: str
) -> None:
"""Initialize AdGuard Home sensor."""
self._state = None
@ -65,12 +63,12 @@ class AdGuardHomeSensor(AdGuardHomeDeviceEntity):
@property
def unique_id(self) -> str:
"""Return the unique ID for this sensor."""
return '_'.join(
return "_".join(
[
DOMAIN,
self.adguard.host,
str(self.adguard.port),
'sensor',
"sensor",
self.measurement,
]
)
@ -92,11 +90,7 @@ class AdGuardHomeDNSQueriesSensor(AdGuardHomeSensor):
def __init__(self, adguard):
"""Initialize AdGuard Home sensor."""
super().__init__(
adguard,
'AdGuard DNS Queries',
'mdi:magnify',
'dns_queries',
'queries',
adguard, "AdGuard DNS Queries", "mdi:magnify", "dns_queries", "queries"
)
async def _adguard_update(self) -> None:
@ -111,10 +105,10 @@ class AdGuardHomeBlockedFilteringSensor(AdGuardHomeSensor):
"""Initialize AdGuard Home sensor."""
super().__init__(
adguard,
'AdGuard DNS Queries Blocked',
'mdi:magnify-close',
'blocked_filtering',
'queries',
"AdGuard DNS Queries Blocked",
"mdi:magnify-close",
"blocked_filtering",
"queries",
)
async def _adguard_update(self) -> None:
@ -129,10 +123,10 @@ class AdGuardHomePercentageBlockedSensor(AdGuardHomeSensor):
"""Initialize AdGuard Home sensor."""
super().__init__(
adguard,
'AdGuard DNS Queries Blocked Ratio',
'mdi:magnify-close',
'blocked_percentage',
'%',
"AdGuard DNS Queries Blocked Ratio",
"mdi:magnify-close",
"blocked_percentage",
"%",
)
async def _adguard_update(self) -> None:
@ -148,10 +142,10 @@ class AdGuardHomeReplacedParentalSensor(AdGuardHomeSensor):
"""Initialize AdGuard Home sensor."""
super().__init__(
adguard,
'AdGuard Parental Control Blocked',
'mdi:human-male-girl',
'blocked_parental',
'requests',
"AdGuard Parental Control Blocked",
"mdi:human-male-girl",
"blocked_parental",
"requests",
)
async def _adguard_update(self) -> None:
@ -166,10 +160,10 @@ class AdGuardHomeReplacedSafeBrowsingSensor(AdGuardHomeSensor):
"""Initialize AdGuard Home sensor."""
super().__init__(
adguard,
'AdGuard Safe Browsing Blocked',
'mdi:shield-half-full',
'blocked_safebrowsing',
'requests',
"AdGuard Safe Browsing Blocked",
"mdi:shield-half-full",
"blocked_safebrowsing",
"requests",
)
async def _adguard_update(self) -> None:
@ -184,10 +178,10 @@ class AdGuardHomeReplacedSafeSearchSensor(AdGuardHomeSensor):
"""Initialize AdGuard Home sensor."""
super().__init__(
adguard,
'Searches Safe Search Enforced',
'mdi:shield-search',
'enforced_safesearch',
'requests',
"Searches Safe Search Enforced",
"mdi:shield-search",
"enforced_safesearch",
"requests",
)
async def _adguard_update(self) -> None:
@ -202,10 +196,10 @@ class AdGuardHomeAverageProcessingTimeSensor(AdGuardHomeSensor):
"""Initialize AdGuard Home sensor."""
super().__init__(
adguard,
'AdGuard Average Processing Speed',
'mdi:speedometer',
'average_speed',
'ms',
"AdGuard Average Processing Speed",
"mdi:speedometer",
"average_speed",
"ms",
)
async def _adguard_update(self) -> None:
@ -220,11 +214,7 @@ class AdGuardHomeRulesCountSensor(AdGuardHomeSensor):
def __init__(self, adguard):
"""Initialize AdGuard Home sensor."""
super().__init__(
adguard,
'AdGuard Rules Count',
'mdi:counter',
'rules_count',
'rules',
adguard, "AdGuard Rules Count", "mdi:counter", "rules_count", "rules"
)
async def _adguard_update(self) -> None:

View file

@ -6,7 +6,10 @@ from adguardhome import AdGuardHomeConnectionError, AdGuardHomeError
from homeassistant.components.adguard import AdGuardHomeDeviceEntity
from homeassistant.components.adguard.const import (
DATA_ADGUARD_CLIENT, DATA_ADGUARD_VERION, DOMAIN)
DATA_ADGUARD_CLIENT,
DATA_ADGUARD_VERION,
DOMAIN,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers.entity import ToggleEntity
@ -19,7 +22,7 @@ PARALLEL_UPDATES = 1
async def async_setup_entry(
hass: HomeAssistantType, entry: ConfigEntry, async_add_entities
hass: HomeAssistantType, entry: ConfigEntry, async_add_entities
) -> None:
"""Set up AdGuard Home switch based on a config entry."""
adguard = hass.data[DOMAIN][DATA_ADGUARD_CLIENT]
@ -54,14 +57,8 @@ class AdGuardHomeSwitch(ToggleEntity, AdGuardHomeDeviceEntity):
@property
def unique_id(self) -> str:
"""Return the unique ID for this sensor."""
return '_'.join(
[
DOMAIN,
self.adguard.host,
str(self.adguard.port),
'switch',
self._key,
]
return "_".join(
[DOMAIN, self.adguard.host, str(self.adguard.port), "switch", self._key]
)
@property
@ -74,9 +71,7 @@ class AdGuardHomeSwitch(ToggleEntity, AdGuardHomeDeviceEntity):
try:
await self._adguard_turn_off()
except AdGuardHomeError:
_LOGGER.error(
"An error occurred while turning off AdGuard Home switch."
)
_LOGGER.error("An error occurred while turning off AdGuard Home switch.")
self._available = False
async def _adguard_turn_off(self) -> None:
@ -88,9 +83,7 @@ class AdGuardHomeSwitch(ToggleEntity, AdGuardHomeDeviceEntity):
try:
await self._adguard_turn_on()
except AdGuardHomeError:
_LOGGER.error(
"An error occurred while turning on AdGuard Home switch."
)
_LOGGER.error("An error occurred while turning on AdGuard Home switch.")
self._available = False
async def _adguard_turn_on(self) -> None:
@ -104,7 +97,7 @@ class AdGuardHomeProtectionSwitch(AdGuardHomeSwitch):
def __init__(self, adguard) -> None:
"""Initialize AdGuard Home switch."""
super().__init__(
adguard, "AdGuard Protection", 'mdi:shield-check', 'protection'
adguard, "AdGuard Protection", "mdi:shield-check", "protection"
)
async def _adguard_turn_off(self) -> None:
@ -126,7 +119,7 @@ class AdGuardHomeParentalSwitch(AdGuardHomeSwitch):
def __init__(self, adguard) -> None:
"""Initialize AdGuard Home switch."""
super().__init__(
adguard, "AdGuard Parental Control", 'mdi:shield-check', 'parental'
adguard, "AdGuard Parental Control", "mdi:shield-check", "parental"
)
async def _adguard_turn_off(self) -> None:
@ -148,7 +141,7 @@ class AdGuardHomeSafeSearchSwitch(AdGuardHomeSwitch):
def __init__(self, adguard) -> None:
"""Initialize AdGuard Home switch."""
super().__init__(
adguard, "AdGuard Safe Search", 'mdi:shield-check', 'safesearch'
adguard, "AdGuard Safe Search", "mdi:shield-check", "safesearch"
)
async def _adguard_turn_off(self) -> None:
@ -170,10 +163,7 @@ class AdGuardHomeSafeBrowsingSwitch(AdGuardHomeSwitch):
def __init__(self, adguard) -> None:
"""Initialize AdGuard Home switch."""
super().__init__(
adguard,
"AdGuard Safe Browsing",
'mdi:shield-check',
'safebrowsing',
adguard, "AdGuard Safe Browsing", "mdi:shield-check", "safebrowsing"
)
async def _adguard_turn_off(self) -> None:
@ -194,9 +184,7 @@ class AdGuardHomeFilteringSwitch(AdGuardHomeSwitch):
def __init__(self, adguard) -> None:
"""Initialize AdGuard Home switch."""
super().__init__(
adguard, "AdGuard Filtering", 'mdi:shield-check', 'filtering'
)
super().__init__(adguard, "AdGuard Filtering", "mdi:shield-check", "filtering")
async def _adguard_turn_off(self) -> None:
"""Turn off the switch."""
@ -216,9 +204,7 @@ class AdGuardHomeQueryLogSwitch(AdGuardHomeSwitch):
def __init__(self, adguard) -> None:
"""Initialize AdGuard Home switch."""
super().__init__(
adguard, "AdGuard Query Log", 'mdi:shield-check', 'querylog'
)
super().__init__(adguard, "AdGuard Query Log", "mdi:shield-check", "querylog")
async def _adguard_turn_off(self) -> None:
"""Turn off the switch."""

View file

@ -10,57 +10,76 @@ import async_timeout
import voluptuous as vol
from homeassistant.const import (
CONF_DEVICE, CONF_IP_ADDRESS, CONF_PORT, EVENT_HOMEASSISTANT_STOP)
CONF_DEVICE,
CONF_IP_ADDRESS,
CONF_PORT,
EVENT_HOMEASSISTANT_STOP,
)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
DATA_ADS = 'data_ads'
DATA_ADS = "data_ads"
# Supported Types
ADSTYPE_BOOL = 'bool'
ADSTYPE_BYTE = 'byte'
ADSTYPE_DINT = 'dint'
ADSTYPE_INT = 'int'
ADSTYPE_UDINT = 'udint'
ADSTYPE_UINT = 'uint'
ADSTYPE_BOOL = "bool"
ADSTYPE_BYTE = "byte"
ADSTYPE_DINT = "dint"
ADSTYPE_INT = "int"
ADSTYPE_UDINT = "udint"
ADSTYPE_UINT = "uint"
CONF_ADS_FACTOR = 'factor'
CONF_ADS_TYPE = 'adstype'
CONF_ADS_VALUE = 'value'
CONF_ADS_VAR = 'adsvar'
CONF_ADS_VAR_BRIGHTNESS = 'adsvar_brightness'
CONF_ADS_VAR_POSITION = 'adsvar_position'
CONF_ADS_FACTOR = "factor"
CONF_ADS_TYPE = "adstype"
CONF_ADS_VALUE = "value"
CONF_ADS_VAR = "adsvar"
CONF_ADS_VAR_BRIGHTNESS = "adsvar_brightness"
CONF_ADS_VAR_POSITION = "adsvar_position"
STATE_KEY_STATE = 'state'
STATE_KEY_BRIGHTNESS = 'brightness'
STATE_KEY_POSITION = 'position'
STATE_KEY_STATE = "state"
STATE_KEY_BRIGHTNESS = "brightness"
STATE_KEY_POSITION = "position"
DOMAIN = 'ads'
DOMAIN = "ads"
SERVICE_WRITE_DATA_BY_NAME = 'write_data_by_name'
SERVICE_WRITE_DATA_BY_NAME = "write_data_by_name"
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_DEVICE): cv.string,
vol.Required(CONF_PORT): cv.port,
vol.Optional(CONF_IP_ADDRESS): cv.string,
})
}, extra=vol.ALLOW_EXTRA)
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{
vol.Required(CONF_DEVICE): cv.string,
vol.Required(CONF_PORT): cv.port,
vol.Optional(CONF_IP_ADDRESS): cv.string,
}
)
},
extra=vol.ALLOW_EXTRA,
)
SCHEMA_SERVICE_WRITE_DATA_BY_NAME = vol.Schema({
vol.Required(CONF_ADS_TYPE):
vol.In([ADSTYPE_INT, ADSTYPE_UINT, ADSTYPE_BYTE, ADSTYPE_BOOL,
ADSTYPE_DINT, ADSTYPE_UDINT]),
vol.Required(CONF_ADS_VALUE): vol.Coerce(int),
vol.Required(CONF_ADS_VAR): cv.string,
})
SCHEMA_SERVICE_WRITE_DATA_BY_NAME = vol.Schema(
{
vol.Required(CONF_ADS_TYPE): vol.In(
[
ADSTYPE_INT,
ADSTYPE_UINT,
ADSTYPE_BYTE,
ADSTYPE_BOOL,
ADSTYPE_DINT,
ADSTYPE_UDINT,
]
),
vol.Required(CONF_ADS_VALUE): vol.Coerce(int),
vol.Required(CONF_ADS_VAR): cv.string,
}
)
def setup(hass, config):
"""Set up the ADS component."""
import pyads
conf = config[DOMAIN]
net_id = conf.get(CONF_DEVICE)
@ -91,7 +110,10 @@ def setup(hass, config):
except pyads.ADSError:
_LOGGER.error(
"Could not connect to ADS host (netid=%s, ip=%s, port=%s)",
net_id, ip_address, port)
net_id,
ip_address,
port,
)
return False
hass.data[DATA_ADS] = ads
@ -109,15 +131,18 @@ def setup(hass, config):
_LOGGER.error(err)
hass.services.register(
DOMAIN, SERVICE_WRITE_DATA_BY_NAME, handle_write_data_by_name,
schema=SCHEMA_SERVICE_WRITE_DATA_BY_NAME)
DOMAIN,
SERVICE_WRITE_DATA_BY_NAME,
handle_write_data_by_name,
schema=SCHEMA_SERVICE_WRITE_DATA_BY_NAME,
)
return True
# Tuple to hold data needed for notification
NotificationItem = namedtuple(
'NotificationItem', 'hnotify huser name plc_datatype callback'
"NotificationItem", "hnotify huser name plc_datatype callback"
)
@ -137,15 +162,17 @@ class AdsHub:
def shutdown(self, *args, **kwargs):
"""Shutdown ADS connection."""
import pyads
_LOGGER.debug("Shutting down ADS")
for notification_item in self._notification_items.values():
_LOGGER.debug(
"Deleting device notification %d, %d",
notification_item.hnotify, notification_item.huser)
notification_item.hnotify,
notification_item.huser,
)
try:
self._client.del_device_notification(
notification_item.hnotify,
notification_item.huser
notification_item.hnotify, notification_item.huser
)
except pyads.ADSError as err:
_LOGGER.error(err)
@ -161,6 +188,7 @@ class AdsHub:
def write_by_name(self, name, value, plc_datatype):
"""Write a value to the device."""
import pyads
with self._lock:
try:
return self._client.write_by_name(name, value, plc_datatype)
@ -170,6 +198,7 @@ class AdsHub:
def read_by_name(self, name, plc_datatype):
"""Read a value from the device."""
import pyads
with self._lock:
try:
return self._client.read_by_name(name, plc_datatype)
@ -179,22 +208,25 @@ class AdsHub:
def add_device_notification(self, name, plc_datatype, callback):
"""Add a notification to the ADS devices."""
import pyads
attr = pyads.NotificationAttrib(ctypes.sizeof(plc_datatype))
with self._lock:
try:
hnotify, huser = self._client.add_device_notification(
name, attr, self._device_notification_callback)
name, attr, self._device_notification_callback
)
except pyads.ADSError as err:
_LOGGER.error("Error subscribing to %s: %s", name, err)
else:
hnotify = int(hnotify)
self._notification_items[hnotify] = NotificationItem(
hnotify, huser, name, plc_datatype, callback)
hnotify, huser, name, plc_datatype, callback
)
_LOGGER.debug(
"Added device notification %d for variable %s",
hnotify, name)
"Added device notification %d for variable %s", hnotify, name
)
def _device_notification_callback(self, notification, name):
"""Handle device notifications."""
@ -213,17 +245,17 @@ class AdsHub:
# Parse data to desired datatype
if notification_item.plc_datatype == self.PLCTYPE_BOOL:
value = bool(struct.unpack('<?', bytearray(data)[:1])[0])
value = bool(struct.unpack("<?", bytearray(data)[:1])[0])
elif notification_item.plc_datatype == self.PLCTYPE_INT:
value = struct.unpack('<h', bytearray(data)[:2])[0]
value = struct.unpack("<h", bytearray(data)[:2])[0]
elif notification_item.plc_datatype == self.PLCTYPE_BYTE:
value = struct.unpack('<B', bytearray(data)[:1])[0]
value = struct.unpack("<B", bytearray(data)[:1])[0]
elif notification_item.plc_datatype == self.PLCTYPE_UINT:
value = struct.unpack('<H', bytearray(data)[:2])[0]
value = struct.unpack("<H", bytearray(data)[:2])[0]
elif notification_item.plc_datatype == self.PLCTYPE_DINT:
value = struct.unpack('<i', bytearray(data)[:4])[0]
value = struct.unpack("<i", bytearray(data)[:4])[0]
elif notification_item.plc_datatype == self.PLCTYPE_UDINT:
value = struct.unpack('<I', bytearray(data)[:4])[0]
value = struct.unpack("<I", bytearray(data)[:4])[0]
else:
value = bytearray(data)
_LOGGER.warning("No callback available for this datatype")
@ -245,11 +277,13 @@ class AdsEntity(Entity):
self._event = None
async def async_initialize_device(
self, ads_var, plctype, state_key=STATE_KEY_STATE, factor=None):
self, ads_var, plctype, state_key=STATE_KEY_STATE, factor=None
):
"""Register device notification."""
def update(name, value):
"""Handle device notifications."""
_LOGGER.debug('Variable %s changed its value to %d', name, value)
_LOGGER.debug("Variable %s changed its value to %d", name, value)
if factor is None:
self._state_dict[state_key] = value
@ -266,14 +300,13 @@ class AdsEntity(Entity):
self._event = asyncio.Event()
await self.hass.async_add_executor_job(
self._ads_hub.add_device_notification,
ads_var, plctype, update)
self._ads_hub.add_device_notification, ads_var, plctype, update
)
try:
with async_timeout.timeout(10):
await self._event.wait()
except asyncio.TimeoutError:
_LOGGER.debug('Variable %s: Timeout during first update',
ads_var)
_LOGGER.debug("Variable %s: Timeout during first update", ads_var)
@property
def name(self):

View file

@ -4,7 +4,10 @@ import logging
import voluptuous as vol
from homeassistant.components.binary_sensor import (
DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA, BinarySensorDevice)
DEVICE_CLASSES_SCHEMA,
PLATFORM_SCHEMA,
BinarySensorDevice,
)
from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME
import homeassistant.helpers.config_validation as cv
@ -12,12 +15,14 @@ from . import CONF_ADS_VAR, DATA_ADS, AdsEntity, STATE_KEY_STATE
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'ADS binary sensor'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_ADS_VAR): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
})
DEFAULT_NAME = "ADS binary sensor"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_ADS_VAR): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
}
)
def setup_platform(hass, config, add_entities, discovery_info=None):
@ -38,12 +43,11 @@ class AdsBinarySensor(AdsEntity, BinarySensorDevice):
def __init__(self, ads_hub, name, ads_var, device_class):
"""Initialize ADS binary sensor."""
super().__init__(ads_hub, name, ads_var)
self._device_class = device_class or 'moving'
self._device_class = device_class or "moving"
async def async_added_to_hass(self):
"""Register device notification."""
await self.async_initialize_device(self._ads_var,
self._ads_hub.PLCTYPE_BOOL)
await self.async_initialize_device(self._ads_var, self._ads_hub.PLCTYPE_BOOL)
@property
def is_on(self):

View file

@ -4,35 +4,48 @@ import logging
import voluptuous as vol
from homeassistant.components.cover import (
PLATFORM_SCHEMA, SUPPORT_OPEN, SUPPORT_CLOSE, SUPPORT_STOP,
SUPPORT_SET_POSITION, ATTR_POSITION, DEVICE_CLASSES_SCHEMA,
CoverDevice)
from homeassistant.const import (
CONF_NAME, CONF_DEVICE_CLASS)
PLATFORM_SCHEMA,
SUPPORT_OPEN,
SUPPORT_CLOSE,
SUPPORT_STOP,
SUPPORT_SET_POSITION,
ATTR_POSITION,
DEVICE_CLASSES_SCHEMA,
CoverDevice,
)
from homeassistant.const import CONF_NAME, CONF_DEVICE_CLASS
import homeassistant.helpers.config_validation as cv
from . import CONF_ADS_VAR, CONF_ADS_VAR_POSITION, DATA_ADS, \
AdsEntity, STATE_KEY_STATE, STATE_KEY_POSITION
from . import (
CONF_ADS_VAR,
CONF_ADS_VAR_POSITION,
DATA_ADS,
AdsEntity,
STATE_KEY_STATE,
STATE_KEY_POSITION,
)
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'ADS Cover'
DEFAULT_NAME = "ADS Cover"
CONF_ADS_VAR_SET_POS = 'adsvar_set_position'
CONF_ADS_VAR_OPEN = 'adsvar_open'
CONF_ADS_VAR_CLOSE = 'adsvar_close'
CONF_ADS_VAR_STOP = 'adsvar_stop'
CONF_ADS_VAR_SET_POS = "adsvar_set_position"
CONF_ADS_VAR_OPEN = "adsvar_open"
CONF_ADS_VAR_CLOSE = "adsvar_close"
CONF_ADS_VAR_STOP = "adsvar_stop"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_ADS_VAR): cv.string,
vol.Optional(CONF_ADS_VAR_POSITION): cv.string,
vol.Optional(CONF_ADS_VAR_SET_POS): cv.string,
vol.Optional(CONF_ADS_VAR_CLOSE): cv.string,
vol.Optional(CONF_ADS_VAR_OPEN): cv.string,
vol.Optional(CONF_ADS_VAR_STOP): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Optional(CONF_ADS_VAR): cv.string,
vol.Optional(CONF_ADS_VAR_POSITION): cv.string,
vol.Optional(CONF_ADS_VAR_SET_POS): cv.string,
vol.Optional(CONF_ADS_VAR_CLOSE): cv.string,
vol.Optional(CONF_ADS_VAR_OPEN): cv.string,
vol.Optional(CONF_ADS_VAR_STOP): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
}
)
def setup_platform(hass, config, add_entities, discovery_info=None):
@ -48,24 +61,38 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
name = config[CONF_NAME]
device_class = config.get(CONF_DEVICE_CLASS)
add_entities([AdsCover(ads_hub,
ads_var_is_closed,
ads_var_position,
ads_var_pos_set,
ads_var_open,
ads_var_close,
ads_var_stop,
name,
device_class)])
add_entities(
[
AdsCover(
ads_hub,
ads_var_is_closed,
ads_var_position,
ads_var_pos_set,
ads_var_open,
ads_var_close,
ads_var_stop,
name,
device_class,
)
]
)
class AdsCover(AdsEntity, CoverDevice):
"""Representation of ADS cover."""
def __init__(self, ads_hub,
ads_var_is_closed, ads_var_position,
ads_var_pos_set, ads_var_open,
ads_var_close, ads_var_stop, name, device_class):
def __init__(
self,
ads_hub,
ads_var_is_closed,
ads_var_position,
ads_var_pos_set,
ads_var_open,
ads_var_close,
ads_var_stop,
name,
device_class,
):
"""Initialize AdsCover entity."""
super().__init__(ads_hub, name, ads_var_is_closed)
if self._ads_var is None:
@ -87,13 +114,14 @@ class AdsCover(AdsEntity, CoverDevice):
async def async_added_to_hass(self):
"""Register device notification."""
if self._ads_var is not None:
await self.async_initialize_device(self._ads_var,
self._ads_hub.PLCTYPE_BOOL)
await self.async_initialize_device(
self._ads_var, self._ads_hub.PLCTYPE_BOOL
)
if self._ads_var_position is not None:
await self.async_initialize_device(self._ads_var_position,
self._ads_hub.PLCTYPE_BYTE,
STATE_KEY_POSITION)
await self.async_initialize_device(
self._ads_var_position, self._ads_hub.PLCTYPE_BYTE, STATE_KEY_POSITION
)
@property
def device_class(self):
@ -130,29 +158,33 @@ class AdsCover(AdsEntity, CoverDevice):
def stop_cover(self, **kwargs):
"""Fire the stop action."""
if self._ads_var_stop:
self._ads_hub.write_by_name(self._ads_var_stop, True,
self._ads_hub.PLCTYPE_BOOL)
self._ads_hub.write_by_name(
self._ads_var_stop, True, self._ads_hub.PLCTYPE_BOOL
)
def set_cover_position(self, **kwargs):
"""Set cover position."""
position = kwargs[ATTR_POSITION]
if self._ads_var_pos_set is not None:
self._ads_hub.write_by_name(self._ads_var_pos_set, position,
self._ads_hub.PLCTYPE_BYTE)
self._ads_hub.write_by_name(
self._ads_var_pos_set, position, self._ads_hub.PLCTYPE_BYTE
)
def open_cover(self, **kwargs):
"""Move the cover up."""
if self._ads_var_open is not None:
self._ads_hub.write_by_name(self._ads_var_open, True,
self._ads_hub.PLCTYPE_BOOL)
self._ads_hub.write_by_name(
self._ads_var_open, True, self._ads_hub.PLCTYPE_BOOL
)
elif self._ads_var_pos_set is not None:
self.set_cover_position(position=100)
def close_cover(self, **kwargs):
"""Move the cover down."""
if self._ads_var_close is not None:
self._ads_hub.write_by_name(self._ads_var_close, True,
self._ads_hub.PLCTYPE_BOOL)
self._ads_hub.write_by_name(
self._ads_var_close, True, self._ads_hub.PLCTYPE_BOOL
)
elif self._ads_var_pos_set is not None:
self.set_cover_position(position=0)
@ -160,6 +192,8 @@ class AdsCover(AdsEntity, CoverDevice):
def available(self):
"""Return False if state has not been updated yet."""
if self._ads_var is not None or self._ads_var_position is not None:
return self._state_dict[STATE_KEY_STATE] is not None or \
self._state_dict[STATE_KEY_POSITION] is not None
return (
self._state_dict[STATE_KEY_STATE] is not None
or self._state_dict[STATE_KEY_POSITION] is not None
)
return True

View file

@ -4,20 +4,32 @@ import logging
import voluptuous as vol
from homeassistant.components.light import (
ATTR_BRIGHTNESS, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, Light)
ATTR_BRIGHTNESS,
PLATFORM_SCHEMA,
SUPPORT_BRIGHTNESS,
Light,
)
from homeassistant.const import CONF_NAME
import homeassistant.helpers.config_validation as cv
from . import CONF_ADS_VAR, CONF_ADS_VAR_BRIGHTNESS, DATA_ADS, \
AdsEntity, STATE_KEY_BRIGHTNESS, STATE_KEY_STATE
from . import (
CONF_ADS_VAR,
CONF_ADS_VAR_BRIGHTNESS,
DATA_ADS,
AdsEntity,
STATE_KEY_BRIGHTNESS,
STATE_KEY_STATE,
)
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'ADS Light'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_ADS_VAR): cv.string,
vol.Optional(CONF_ADS_VAR_BRIGHTNESS): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string
})
DEFAULT_NAME = "ADS Light"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_ADS_VAR): cv.string,
vol.Optional(CONF_ADS_VAR_BRIGHTNESS): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
}
)
def setup_platform(hass, config, add_entities, discovery_info=None):
@ -28,8 +40,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
ads_var_brightness = config.get(CONF_ADS_VAR_BRIGHTNESS)
name = config.get(CONF_NAME)
add_entities([AdsLight(ads_hub, ads_var_enable, ads_var_brightness,
name)])
add_entities([AdsLight(ads_hub, ads_var_enable, ads_var_brightness, name)])
class AdsLight(AdsEntity, Light):
@ -43,13 +54,14 @@ class AdsLight(AdsEntity, Light):
async def async_added_to_hass(self):
"""Register device notification."""
await self.async_initialize_device(self._ads_var,
self._ads_hub.PLCTYPE_BOOL)
await self.async_initialize_device(self._ads_var, self._ads_hub.PLCTYPE_BOOL)
if self._ads_var_brightness is not None:
await self.async_initialize_device(self._ads_var_brightness,
self._ads_hub.PLCTYPE_UINT,
STATE_KEY_BRIGHTNESS)
await self.async_initialize_device(
self._ads_var_brightness,
self._ads_hub.PLCTYPE_UINT,
STATE_KEY_BRIGHTNESS,
)
@property
def brightness(self):
@ -72,14 +84,13 @@ class AdsLight(AdsEntity, Light):
def turn_on(self, **kwargs):
"""Turn the light on or set a specific dimmer value."""
brightness = kwargs.get(ATTR_BRIGHTNESS)
self._ads_hub.write_by_name(self._ads_var, True,
self._ads_hub.PLCTYPE_BOOL)
self._ads_hub.write_by_name(self._ads_var, True, self._ads_hub.PLCTYPE_BOOL)
if self._ads_var_brightness is not None and brightness is not None:
self._ads_hub.write_by_name(self._ads_var_brightness, brightness,
self._ads_hub.PLCTYPE_UINT)
self._ads_hub.write_by_name(
self._ads_var_brightness, brightness, self._ads_hub.PLCTYPE_UINT
)
def turn_off(self, **kwargs):
"""Turn the light off."""
self._ads_hub.write_by_name(self._ads_var, False,
self._ads_hub.PLCTYPE_BOOL)
self._ads_hub.write_by_name(self._ads_var, False, self._ads_hub.PLCTYPE_BOOL)

View file

@ -8,21 +8,28 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT
import homeassistant.helpers.config_validation as cv
from . import CONF_ADS_FACTOR, CONF_ADS_TYPE, CONF_ADS_VAR, \
AdsEntity, STATE_KEY_STATE
from . import CONF_ADS_FACTOR, CONF_ADS_TYPE, CONF_ADS_VAR, AdsEntity, STATE_KEY_STATE
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = "ADS sensor"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_ADS_VAR): cv.string,
vol.Optional(CONF_ADS_FACTOR): cv.positive_int,
vol.Optional(CONF_ADS_TYPE, default=ads.ADSTYPE_INT):
vol.In([ads.ADSTYPE_INT, ads.ADSTYPE_UINT, ads.ADSTYPE_BYTE,
ads.ADSTYPE_DINT, ads.ADSTYPE_UDINT]),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_UNIT_OF_MEASUREMENT, default=''): cv.string,
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_ADS_VAR): cv.string,
vol.Optional(CONF_ADS_FACTOR): cv.positive_int,
vol.Optional(CONF_ADS_TYPE, default=ads.ADSTYPE_INT): vol.In(
[
ads.ADSTYPE_INT,
ads.ADSTYPE_UINT,
ads.ADSTYPE_BYTE,
ads.ADSTYPE_DINT,
ads.ADSTYPE_UDINT,
]
),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_UNIT_OF_MEASUREMENT, default=""): cv.string,
}
)
def setup_platform(hass, config, add_entities, discovery_info=None):
@ -35,8 +42,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
unit_of_measurement = config.get(CONF_UNIT_OF_MEASUREMENT)
factor = config.get(CONF_ADS_FACTOR)
entity = AdsSensor(
ads_hub, ads_var, ads_type, name, unit_of_measurement, factor)
entity = AdsSensor(ads_hub, ads_var, ads_type, name, unit_of_measurement, factor)
add_entities([entity])
@ -44,8 +50,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
class AdsSensor(AdsEntity):
"""Representation of an ADS sensor entity."""
def __init__(self, ads_hub, ads_var, ads_type, name, unit_of_measurement,
factor):
def __init__(self, ads_hub, ads_var, ads_type, name, unit_of_measurement, factor):
"""Initialize AdsSensor entity."""
super().__init__(ads_hub, name, ads_var)
self._unit_of_measurement = unit_of_measurement
@ -58,7 +63,8 @@ class AdsSensor(AdsEntity):
self._ads_var,
self._ads_hub.ADS_TYPEMAP[self._ads_type],
STATE_KEY_STATE,
self._factor)
self._factor,
)
@property
def state(self):

View file

@ -11,12 +11,11 @@ from . import CONF_ADS_VAR, DATA_ADS, AdsEntity, STATE_KEY_STATE
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'ADS Switch'
DEFAULT_NAME = "ADS Switch"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_ADS_VAR): cv.string,
vol.Optional(CONF_NAME): cv.string,
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{vol.Required(CONF_ADS_VAR): cv.string, vol.Optional(CONF_NAME): cv.string}
)
def setup_platform(hass, config, add_entities, discovery_info=None):
@ -34,8 +33,7 @@ class AdsSwitch(AdsEntity, SwitchDevice):
async def async_added_to_hass(self):
"""Register device notification."""
await self.async_initialize_device(self._ads_var,
self._ads_hub.PLCTYPE_BOOL)
await self.async_initialize_device(self._ads_var, self._ads_hub.PLCTYPE_BOOL)
@property
def is_on(self):
@ -44,10 +42,8 @@ class AdsSwitch(AdsEntity, SwitchDevice):
def turn_on(self, **kwargs):
"""Turn the switch on."""
self._ads_hub.write_by_name(
self._ads_var, True, self._ads_hub.PLCTYPE_BOOL)
self._ads_hub.write_by_name(self._ads_var, True, self._ads_hub.PLCTYPE_BOOL)
def turn_off(self, **kwargs):
"""Turn the switch off."""
self._ads_hub.write_by_name(
self._ads_var, False, self._ads_hub.PLCTYPE_BOOL)
self._ads_hub.write_by_name(self._ads_var, False, self._ads_hub.PLCTYPE_BOOL)

View file

@ -1,2 +1,2 @@
"""Constants for the Aftership integration."""
DOMAIN = 'aftership'
DOMAIN = "aftership"

View file

@ -15,24 +15,24 @@ from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
ATTRIBUTION = 'Information provided by AfterShip'
ATTR_TRACKINGS = 'trackings'
ATTRIBUTION = "Information provided by AfterShip"
ATTR_TRACKINGS = "trackings"
BASE = 'https://track.aftership.com/'
BASE = "https://track.aftership.com/"
CONF_SLUG = 'slug'
CONF_TITLE = 'title'
CONF_TRACKING_NUMBER = 'tracking_number'
CONF_SLUG = "slug"
CONF_TITLE = "title"
CONF_TRACKING_NUMBER = "tracking_number"
DEFAULT_NAME = 'aftership'
UPDATE_TOPIC = DOMAIN + '_update'
DEFAULT_NAME = "aftership"
UPDATE_TOPIC = DOMAIN + "_update"
ICON = 'mdi:package-variant-closed'
ICON = "mdi:package-variant-closed"
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5)
SERVICE_ADD_TRACKING = 'add_tracking'
SERVICE_REMOVE_TRACKING = 'remove_tracking'
SERVICE_ADD_TRACKING = "add_tracking"
SERVICE_REMOVE_TRACKING = "remove_tracking"
ADD_TRACKING_SERVICE_SCHEMA = vol.Schema(
{
@ -43,18 +43,18 @@ ADD_TRACKING_SERVICE_SCHEMA = vol.Schema(
)
REMOVE_TRACKING_SERVICE_SCHEMA = vol.Schema(
{vol.Required(CONF_SLUG): cv.string,
vol.Required(CONF_TRACKING_NUMBER): cv.string}
{vol.Required(CONF_SLUG): cv.string, vol.Required(CONF_TRACKING_NUMBER): cv.string}
)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_API_KEY): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_API_KEY): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
}
)
async def async_setup_platform(
hass, config, async_add_entities, discovery_info=None):
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the AfterShip sensor platform."""
from pyaftership.tracker import Tracking
@ -66,9 +66,10 @@ async def async_setup_platform(
await aftership.get_trackings()
if not aftership.meta or aftership.meta['code'] != 200:
_LOGGER.error("No tracking data found. Check API key is correct: %s",
aftership.meta)
if not aftership.meta or aftership.meta["code"] != 200:
_LOGGER.error(
"No tracking data found. Check API key is correct: %s", aftership.meta
)
return
instance = AfterShipSensor(aftership, name)
@ -130,7 +131,7 @@ class AfterShipSensor(Entity):
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this entity, if any."""
return 'packages'
return "packages"
@property
def device_state_attributes(self):
@ -145,7 +146,8 @@ class AfterShipSensor(Entity):
async def async_added_to_hass(self):
"""Register callbacks."""
self.hass.helpers.dispatcher.async_dispatcher_connect(
UPDATE_TOPIC, self.force_update)
UPDATE_TOPIC, self.force_update
)
async def force_update(self):
"""Force update of data."""
@ -160,40 +162,40 @@ class AfterShipSensor(Entity):
if not self.aftership.meta:
_LOGGER.error("Unknown errors when querying")
return
if self.aftership.meta['code'] != 200:
if self.aftership.meta["code"] != 200:
_LOGGER.error(
"Errors when querying AfterShip. %s", str(self.aftership.meta))
"Errors when querying AfterShip. %s", str(self.aftership.meta)
)
return
status_to_ignore = {'delivered'}
status_to_ignore = {"delivered"}
status_counts = {}
trackings = []
not_delivered_count = 0
for track in self.aftership.trackings['trackings']:
status = track['tag'].lower()
for track in self.aftership.trackings["trackings"]:
status = track["tag"].lower()
name = (
track['tracking_number']
if track['title'] is None
else track['title']
track["tracking_number"] if track["title"] is None else track["title"]
)
last_checkpoint = (
"Shipment pending"
if track['tag'] == "Pending"
else track['checkpoints'][-1]
if track["tag"] == "Pending"
else track["checkpoints"][-1]
)
status_counts[status] = status_counts.get(status, 0) + 1
trackings.append({
'name': name,
'tracking_number': track['tracking_number'],
'slug': track['slug'],
'link': '%s%s/%s' %
(BASE, track['slug'], track['tracking_number']),
'last_update': track['updated_at'],
'expected_delivery': track['expected_delivery'],
'status': track['tag'],
'last_checkpoint': last_checkpoint
})
trackings.append(
{
"name": name,
"tracking_number": track["tracking_number"],
"slug": track["slug"],
"link": "%s%s/%s" % (BASE, track["slug"], track["tracking_number"]),
"last_update": track["updated_at"],
"expected_delivery": track["expected_delivery"],
"status": track["tag"],
"last_checkpoint": last_checkpoint,
}
)
if status not in status_to_ignore:
not_delivered_count += 1

View file

@ -4,50 +4,53 @@ import logging
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.config_validation import ( # noqa
PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE)
PLATFORM_SCHEMA,
PLATFORM_SCHEMA_BASE,
)
from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
ATTR_AQI = 'air_quality_index'
ATTR_ATTRIBUTION = 'attribution'
ATTR_CO2 = 'carbon_dioxide'
ATTR_CO = 'carbon_monoxide'
ATTR_N2O = 'nitrogen_oxide'
ATTR_NO = 'nitrogen_monoxide'
ATTR_NO2 = 'nitrogen_dioxide'
ATTR_OZONE = 'ozone'
ATTR_PM_0_1 = 'particulate_matter_0_1'
ATTR_PM_10 = 'particulate_matter_10'
ATTR_PM_2_5 = 'particulate_matter_2_5'
ATTR_SO2 = 'sulphur_dioxide'
ATTR_AQI = "air_quality_index"
ATTR_ATTRIBUTION = "attribution"
ATTR_CO2 = "carbon_dioxide"
ATTR_CO = "carbon_monoxide"
ATTR_N2O = "nitrogen_oxide"
ATTR_NO = "nitrogen_monoxide"
ATTR_NO2 = "nitrogen_dioxide"
ATTR_OZONE = "ozone"
ATTR_PM_0_1 = "particulate_matter_0_1"
ATTR_PM_10 = "particulate_matter_10"
ATTR_PM_2_5 = "particulate_matter_2_5"
ATTR_SO2 = "sulphur_dioxide"
DOMAIN = 'air_quality'
DOMAIN = "air_quality"
ENTITY_ID_FORMAT = DOMAIN + '.{}'
ENTITY_ID_FORMAT = DOMAIN + ".{}"
SCAN_INTERVAL = timedelta(seconds=30)
PROP_TO_ATTR = {
'air_quality_index': ATTR_AQI,
'attribution': ATTR_ATTRIBUTION,
'carbon_dioxide': ATTR_CO2,
'carbon_monoxide': ATTR_CO,
'nitrogen_oxide': ATTR_N2O,
'nitrogen_monoxide': ATTR_NO,
'nitrogen_dioxide': ATTR_NO2,
'ozone': ATTR_OZONE,
'particulate_matter_0_1': ATTR_PM_0_1,
'particulate_matter_10': ATTR_PM_10,
'particulate_matter_2_5': ATTR_PM_2_5,
'sulphur_dioxide': ATTR_SO2,
"air_quality_index": ATTR_AQI,
"attribution": ATTR_ATTRIBUTION,
"carbon_dioxide": ATTR_CO2,
"carbon_monoxide": ATTR_CO,
"nitrogen_oxide": ATTR_N2O,
"nitrogen_monoxide": ATTR_NO,
"nitrogen_dioxide": ATTR_NO2,
"ozone": ATTR_OZONE,
"particulate_matter_0_1": ATTR_PM_0_1,
"particulate_matter_10": ATTR_PM_10,
"particulate_matter_2_5": ATTR_PM_2_5,
"sulphur_dioxide": ATTR_SO2,
}
async def async_setup(hass, config):
"""Set up the air quality component."""
component = hass.data[DOMAIN] = EntityComponent(
_LOGGER, DOMAIN, hass, SCAN_INTERVAL)
_LOGGER, DOMAIN, hass, SCAN_INTERVAL
)
await component.async_setup(config)
return True

View file

@ -6,118 +6,96 @@ import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (
ATTR_ATTRIBUTION, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_API_KEY,
CONF_LATITUDE, CONF_LONGITUDE, CONF_MONITORED_CONDITIONS,
CONF_SCAN_INTERVAL, CONF_STATE, CONF_SHOW_ON_MAP)
ATTR_ATTRIBUTION,
ATTR_LATITUDE,
ATTR_LONGITUDE,
CONF_API_KEY,
CONF_LATITUDE,
CONF_LONGITUDE,
CONF_MONITORED_CONDITIONS,
CONF_SCAN_INTERVAL,
CONF_STATE,
CONF_SHOW_ON_MAP,
)
from homeassistant.helpers import aiohttp_client, config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
_LOGGER = getLogger(__name__)
ATTR_CITY = 'city'
ATTR_COUNTRY = 'country'
ATTR_POLLUTANT_SYMBOL = 'pollutant_symbol'
ATTR_POLLUTANT_UNIT = 'pollutant_unit'
ATTR_REGION = 'region'
ATTR_CITY = "city"
ATTR_COUNTRY = "country"
ATTR_POLLUTANT_SYMBOL = "pollutant_symbol"
ATTR_POLLUTANT_UNIT = "pollutant_unit"
ATTR_REGION = "region"
CONF_CITY = 'city'
CONF_COUNTRY = 'country'
CONF_CITY = "city"
CONF_COUNTRY = "country"
DEFAULT_ATTRIBUTION = "Data provided by AirVisual"
DEFAULT_SCAN_INTERVAL = timedelta(minutes=10)
MASS_PARTS_PER_MILLION = 'ppm'
MASS_PARTS_PER_BILLION = 'ppb'
VOLUME_MICROGRAMS_PER_CUBIC_METER = 'µg/m3'
MASS_PARTS_PER_MILLION = "ppm"
MASS_PARTS_PER_BILLION = "ppb"
VOLUME_MICROGRAMS_PER_CUBIC_METER = "µg/m3"
SENSOR_TYPE_LEVEL = 'air_pollution_level'
SENSOR_TYPE_AQI = 'air_quality_index'
SENSOR_TYPE_POLLUTANT = 'main_pollutant'
SENSOR_TYPE_LEVEL = "air_pollution_level"
SENSOR_TYPE_AQI = "air_quality_index"
SENSOR_TYPE_POLLUTANT = "main_pollutant"
SENSORS = [
(SENSOR_TYPE_LEVEL, 'Air Pollution Level', 'mdi:gauge', None),
(SENSOR_TYPE_AQI, 'Air Quality Index', 'mdi:chart-line', 'AQI'),
(SENSOR_TYPE_POLLUTANT, 'Main Pollutant', 'mdi:chemical-weapon', None),
(SENSOR_TYPE_LEVEL, "Air Pollution Level", "mdi:gauge", None),
(SENSOR_TYPE_AQI, "Air Quality Index", "mdi:chart-line", "AQI"),
(SENSOR_TYPE_POLLUTANT, "Main Pollutant", "mdi:chemical-weapon", None),
]
POLLUTANT_LEVEL_MAPPING = [{
'label': 'Good',
'icon': 'mdi:emoticon-excited',
'minimum': 0,
'maximum': 50
}, {
'label': 'Moderate',
'icon': 'mdi:emoticon-happy',
'minimum': 51,
'maximum': 100
}, {
'label': 'Unhealthy for sensitive groups',
'icon': 'mdi:emoticon-neutral',
'minimum': 101,
'maximum': 150
}, {
'label': 'Unhealthy',
'icon': 'mdi:emoticon-sad',
'minimum': 151,
'maximum': 200
}, {
'label': 'Very Unhealthy',
'icon': 'mdi:emoticon-dead',
'minimum': 201,
'maximum': 300
}, {
'label': 'Hazardous',
'icon': 'mdi:biohazard',
'minimum': 301,
'maximum': 10000
}]
POLLUTANT_LEVEL_MAPPING = [
{"label": "Good", "icon": "mdi:emoticon-excited", "minimum": 0, "maximum": 50},
{"label": "Moderate", "icon": "mdi:emoticon-happy", "minimum": 51, "maximum": 100},
{
"label": "Unhealthy for sensitive groups",
"icon": "mdi:emoticon-neutral",
"minimum": 101,
"maximum": 150,
},
{"label": "Unhealthy", "icon": "mdi:emoticon-sad", "minimum": 151, "maximum": 200},
{
"label": "Very Unhealthy",
"icon": "mdi:emoticon-dead",
"minimum": 201,
"maximum": 300,
},
{"label": "Hazardous", "icon": "mdi:biohazard", "minimum": 301, "maximum": 10000},
]
POLLUTANT_MAPPING = {
'co': {
'label': 'Carbon Monoxide',
'unit': MASS_PARTS_PER_MILLION
},
'n2': {
'label': 'Nitrogen Dioxide',
'unit': MASS_PARTS_PER_BILLION
},
'o3': {
'label': 'Ozone',
'unit': MASS_PARTS_PER_BILLION
},
'p1': {
'label': 'PM10',
'unit': VOLUME_MICROGRAMS_PER_CUBIC_METER
},
'p2': {
'label': 'PM2.5',
'unit': VOLUME_MICROGRAMS_PER_CUBIC_METER
},
's2': {
'label': 'Sulfur Dioxide',
'unit': MASS_PARTS_PER_BILLION
},
"co": {"label": "Carbon Monoxide", "unit": MASS_PARTS_PER_MILLION},
"n2": {"label": "Nitrogen Dioxide", "unit": MASS_PARTS_PER_BILLION},
"o3": {"label": "Ozone", "unit": MASS_PARTS_PER_BILLION},
"p1": {"label": "PM10", "unit": VOLUME_MICROGRAMS_PER_CUBIC_METER},
"p2": {"label": "PM2.5", "unit": VOLUME_MICROGRAMS_PER_CUBIC_METER},
"s2": {"label": "Sulfur Dioxide", "unit": MASS_PARTS_PER_BILLION},
}
SENSOR_LOCALES = {'cn': 'Chinese', 'us': 'U.S.'}
SENSOR_LOCALES = {"cn": "Chinese", "us": "U.S."}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_API_KEY): cv.string,
vol.Required(CONF_MONITORED_CONDITIONS, default=list(SENSOR_LOCALES)):
vol.All(cv.ensure_list, [vol.In(SENSOR_LOCALES)]),
vol.Inclusive(CONF_CITY, 'city'): cv.string,
vol.Inclusive(CONF_COUNTRY, 'city'): cv.string,
vol.Inclusive(CONF_LATITUDE, 'coords'): cv.latitude,
vol.Inclusive(CONF_LONGITUDE, 'coords'): cv.longitude,
vol.Optional(CONF_SHOW_ON_MAP, default=True): cv.boolean,
vol.Inclusive(CONF_STATE, 'city'): cv.string,
vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL):
cv.time_period
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_API_KEY): cv.string,
vol.Required(CONF_MONITORED_CONDITIONS, default=list(SENSOR_LOCALES)): vol.All(
cv.ensure_list, [vol.In(SENSOR_LOCALES)]
),
vol.Inclusive(CONF_CITY, "city"): cv.string,
vol.Inclusive(CONF_COUNTRY, "city"): cv.string,
vol.Inclusive(CONF_LATITUDE, "coords"): cv.latitude,
vol.Inclusive(CONF_LONGITUDE, "coords"): cv.longitude,
vol.Optional(CONF_SHOW_ON_MAP, default=True): cv.boolean,
vol.Inclusive(CONF_STATE, "city"): cv.string,
vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): cv.time_period,
}
)
async def async_setup_platform(
hass, config, async_add_entities, discovery_info=None):
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Configure the platform and add the sensors."""
from pyairvisual import Client
@ -132,25 +110,27 @@ async def async_setup_platform(
if city and state and country:
_LOGGER.debug(
"Using city, state, and country: %s, %s, %s", city, state, country)
location_id = ','.join((city, state, country))
"Using city, state, and country: %s, %s, %s", city, state, country
)
location_id = ",".join((city, state, country))
data = AirVisualData(
Client(websession, api_key=config[CONF_API_KEY]),
city=city,
state=state,
country=country,
show_on_map=config[CONF_SHOW_ON_MAP],
scan_interval=config[CONF_SCAN_INTERVAL])
scan_interval=config[CONF_SCAN_INTERVAL],
)
else:
_LOGGER.debug(
"Using latitude and longitude: %s, %s", latitude, longitude)
location_id = ','.join((str(latitude), str(longitude)))
_LOGGER.debug("Using latitude and longitude: %s, %s", latitude, longitude)
location_id = ",".join((str(latitude), str(longitude)))
data = AirVisualData(
Client(websession, api_key=config[CONF_API_KEY]),
latitude=latitude,
longitude=longitude,
show_on_map=config[CONF_SHOW_ON_MAP],
scan_interval=config[CONF_SCAN_INTERVAL])
scan_interval=config[CONF_SCAN_INTERVAL],
)
await data.async_update()
@ -158,8 +138,8 @@ async def async_setup_platform(
for locale in config[CONF_MONITORED_CONDITIONS]:
for kind, name, icon, unit in SENSORS:
sensors.append(
AirVisualSensor(
data, kind, name, icon, unit, locale, location_id))
AirVisualSensor(data, kind, name, icon, unit, locale, location_id)
)
async_add_entities(sensors, True)
@ -186,8 +166,8 @@ class AirVisualSensor(Entity):
self._attrs[ATTR_LATITUDE] = self.airvisual.latitude
self._attrs[ATTR_LONGITUDE] = self.airvisual.longitude
else:
self._attrs['lati'] = self.airvisual.latitude
self._attrs['long'] = self.airvisual.longitude
self._attrs["lati"] = self.airvisual.latitude
self._attrs["long"] = self.airvisual.longitude
return self._attrs
@ -204,7 +184,7 @@ class AirVisualSensor(Entity):
@property
def name(self):
"""Return the name."""
return '{0} {1}'.format(SENSOR_LOCALES[self._locale], self._name)
return "{0} {1}".format(SENSOR_LOCALES[self._locale], self._name)
@property
def state(self):
@ -214,8 +194,7 @@ class AirVisualSensor(Entity):
@property
def unique_id(self):
"""Return a unique, HASS-friendly identifier for this entity."""
return '{0}_{1}_{2}'.format(
self._location_id, self._locale, self._type)
return "{0}_{1}_{2}".format(self._location_id, self._locale, self._type)
@property
def unit_of_measurement(self):
@ -231,22 +210,25 @@ class AirVisualSensor(Entity):
return
if self._type == SENSOR_TYPE_LEVEL:
aqi = data['aqi{0}'.format(self._locale)]
aqi = data["aqi{0}".format(self._locale)]
[level] = [
i for i in POLLUTANT_LEVEL_MAPPING
if i['minimum'] <= aqi <= i['maximum']
i
for i in POLLUTANT_LEVEL_MAPPING
if i["minimum"] <= aqi <= i["maximum"]
]
self._state = level['label']
self._icon = level['icon']
self._state = level["label"]
self._icon = level["icon"]
elif self._type == SENSOR_TYPE_AQI:
self._state = data['aqi{0}'.format(self._locale)]
self._state = data["aqi{0}".format(self._locale)]
elif self._type == SENSOR_TYPE_POLLUTANT:
symbol = data['main{0}'.format(self._locale)]
self._state = POLLUTANT_MAPPING[symbol]['label']
self._attrs.update({
ATTR_POLLUTANT_SYMBOL: symbol,
ATTR_POLLUTANT_UNIT: POLLUTANT_MAPPING[symbol]['unit']
})
symbol = data["main{0}".format(self._locale)]
self._state = POLLUTANT_MAPPING[symbol]["label"]
self._attrs.update(
{
ATTR_POLLUTANT_SYMBOL: symbol,
ATTR_POLLUTANT_UNIT: POLLUTANT_MAPPING[symbol]["unit"],
}
)
class AirVisualData:
@ -263,8 +245,7 @@ class AirVisualData:
self.show_on_map = kwargs.get(CONF_SHOW_ON_MAP)
self.state = kwargs.get(CONF_STATE)
self.async_update = Throttle(
kwargs[CONF_SCAN_INTERVAL])(self._async_update)
self.async_update = Throttle(kwargs[CONF_SCAN_INTERVAL])(self._async_update)
async def _async_update(self):
"""Update AirVisual data."""
@ -272,23 +253,21 @@ class AirVisualData:
try:
if self.city and self.state and self.country:
resp = await self._client.api.city(
self.city, self.state, self.country)
self.longitude, self.latitude = resp['location']['coordinates']
resp = await self._client.api.city(self.city, self.state, self.country)
self.longitude, self.latitude = resp["location"]["coordinates"]
else:
resp = await self._client.api.nearest_city(
self.latitude, self.longitude)
self.latitude, self.longitude
)
_LOGGER.debug("New data retrieved: %s", resp)
self.pollution_info = resp['current']['pollution']
self.pollution_info = resp["current"]["pollution"]
except (KeyError, AirVisualError) as err:
if self.city and self.state and self.country:
location = (self.city, self.state, self.country)
else:
location = (self.latitude, self.longitude)
_LOGGER.error(
"Can't retrieve data for location: %s (%s)", location,
err)
_LOGGER.error("Can't retrieve data for location: %s (%s)", location, err)
self.pollution_info = {}

View file

@ -3,30 +3,39 @@ import logging
import voluptuous as vol
from homeassistant.components.cover import (CoverDevice, PLATFORM_SCHEMA,
SUPPORT_OPEN, SUPPORT_CLOSE)
from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD, STATE_CLOSED,
STATE_OPENING, STATE_CLOSING, STATE_OPEN)
from homeassistant.components.cover import (
CoverDevice,
PLATFORM_SCHEMA,
SUPPORT_OPEN,
SUPPORT_CLOSE,
)
from homeassistant.const import (
CONF_USERNAME,
CONF_PASSWORD,
STATE_CLOSED,
STATE_OPENING,
STATE_CLOSING,
STATE_OPEN,
)
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
NOTIFICATION_ID = 'aladdin_notification'
NOTIFICATION_TITLE = 'Aladdin Connect Cover Setup'
NOTIFICATION_ID = "aladdin_notification"
NOTIFICATION_TITLE = "Aladdin Connect Cover Setup"
STATES_MAP = {
'open': STATE_OPEN,
'opening': STATE_OPENING,
'closed': STATE_CLOSED,
'closing': STATE_CLOSING
"open": STATE_OPEN,
"opening": STATE_OPENING,
"closed": STATE_CLOSED,
"closing": STATE_CLOSING,
}
SUPPORTED_FEATURES = SUPPORT_OPEN | SUPPORT_CLOSE
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string}
)
def setup_platform(hass, config, add_entities, discovery_info=None):
@ -44,11 +53,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
except (TypeError, KeyError, NameError, ValueError) as ex:
_LOGGER.error("%s", ex)
hass.components.persistent_notification.create(
'Error: {}<br />'
'You will need to restart hass after fixing.'
''.format(ex),
"Error: {}<br />"
"You will need to restart hass after fixing."
"".format(ex),
title=NOTIFICATION_TITLE,
notification_id=NOTIFICATION_ID)
notification_id=NOTIFICATION_ID,
)
class AladdinDevice(CoverDevice):
@ -57,15 +67,15 @@ class AladdinDevice(CoverDevice):
def __init__(self, acc, device):
"""Initialize the cover."""
self._acc = acc
self._device_id = device['device_id']
self._number = device['door_number']
self._name = device['name']
self._status = STATES_MAP.get(device['status'])
self._device_id = device["device_id"]
self._number = device["door_number"]
self._name = device["name"]
self._status = STATES_MAP.get(device["status"])
@property
def device_class(self):
"""Define this cover as a garage door."""
return 'garage'
return "garage"
@property
def supported_features(self):
@ -75,7 +85,7 @@ class AladdinDevice(CoverDevice):
@property
def unique_id(self):
"""Return a unique ID."""
return '{}-{}'.format(self._device_id, self._number)
return "{}-{}".format(self._device_id, self._number)
@property
def name(self):

View file

@ -5,59 +5,65 @@ import logging
import voluptuous as vol
from homeassistant.const import (
ATTR_CODE, ATTR_CODE_FORMAT, SERVICE_ALARM_TRIGGER, SERVICE_ALARM_DISARM,
SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY, SERVICE_ALARM_ARM_NIGHT,
SERVICE_ALARM_ARM_CUSTOM_BYPASS)
ATTR_CODE,
ATTR_CODE_FORMAT,
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.helpers.config_validation import ( # noqa
ENTITY_SERVICE_SCHEMA, PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE)
ENTITY_SERVICE_SCHEMA,
PLATFORM_SCHEMA,
PLATFORM_SCHEMA_BASE,
)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent
DOMAIN = 'alarm_control_panel'
DOMAIN = "alarm_control_panel"
SCAN_INTERVAL = timedelta(seconds=30)
ATTR_CHANGED_BY = 'changed_by'
FORMAT_TEXT = 'text'
FORMAT_NUMBER = 'number'
ATTR_CODE_ARM_REQUIRED = 'code_arm_required'
ATTR_CHANGED_BY = "changed_by"
FORMAT_TEXT = "text"
FORMAT_NUMBER = "number"
ATTR_CODE_ARM_REQUIRED = "code_arm_required"
ENTITY_ID_FORMAT = DOMAIN + '.{}'
ENTITY_ID_FORMAT = DOMAIN + ".{}"
ALARM_SERVICE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend({
vol.Optional(ATTR_CODE): cv.string,
})
ALARM_SERVICE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend(
{vol.Optional(ATTR_CODE): cv.string}
)
async def async_setup(hass, config):
"""Track states and offer events for sensors."""
component = hass.data[DOMAIN] = EntityComponent(
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL)
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL
)
await component.async_setup(config)
component.async_register_entity_service(
SERVICE_ALARM_DISARM, ALARM_SERVICE_SCHEMA,
'async_alarm_disarm'
SERVICE_ALARM_DISARM, ALARM_SERVICE_SCHEMA, "async_alarm_disarm"
)
component.async_register_entity_service(
SERVICE_ALARM_ARM_HOME, ALARM_SERVICE_SCHEMA,
'async_alarm_arm_home'
SERVICE_ALARM_ARM_HOME, ALARM_SERVICE_SCHEMA, "async_alarm_arm_home"
)
component.async_register_entity_service(
SERVICE_ALARM_ARM_AWAY, ALARM_SERVICE_SCHEMA,
'async_alarm_arm_away'
SERVICE_ALARM_ARM_AWAY, ALARM_SERVICE_SCHEMA, "async_alarm_arm_away"
)
component.async_register_entity_service(
SERVICE_ALARM_ARM_NIGHT, ALARM_SERVICE_SCHEMA,
'async_alarm_arm_night'
SERVICE_ALARM_ARM_NIGHT, ALARM_SERVICE_SCHEMA, "async_alarm_arm_night"
)
component.async_register_entity_service(
SERVICE_ALARM_ARM_CUSTOM_BYPASS, ALARM_SERVICE_SCHEMA,
'async_alarm_arm_custom_bypass'
SERVICE_ALARM_ARM_CUSTOM_BYPASS,
ALARM_SERVICE_SCHEMA,
"async_alarm_arm_custom_bypass",
)
component.async_register_entity_service(
SERVICE_ALARM_TRIGGER, ALARM_SERVICE_SCHEMA,
'async_alarm_trigger'
SERVICE_ALARM_TRIGGER, ALARM_SERVICE_SCHEMA, "async_alarm_trigger"
)
return True
@ -156,8 +162,7 @@ class AlarmControlPanel(Entity):
This method must be run in the event loop and returns a coroutine.
"""
return self.hass.async_add_executor_job(
self.alarm_arm_custom_bypass, code)
return self.hass.async_add_executor_job(self.alarm_arm_custom_bypass, code)
@property
def state_attributes(self):
@ -165,6 +170,6 @@ class AlarmControlPanel(Entity):
state_attr = {
ATTR_CODE_FORMAT: self.code_format,
ATTR_CHANGED_BY: self.changed_by,
ATTR_CODE_ARM_REQUIRED: self.code_arm_required
ATTR_CODE_ARM_REQUIRED: self.code_arm_required,
}
return state_attr

View file

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

View file

@ -5,18 +5,20 @@ import voluptuous as vol
import homeassistant.components.alarm_control_panel as alarm
from homeassistant.const import (
ATTR_CODE, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED)
ATTR_CODE,
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_DISARMED,
STATE_ALARM_TRIGGERED,
)
import homeassistant.helpers.config_validation as cv
from . import DATA_AD, SIGNAL_PANEL_MESSAGE
_LOGGER = logging.getLogger(__name__)
SERVICE_ALARM_TOGGLE_CHIME = 'alarmdecoder_alarm_toggle_chime'
ALARM_TOGGLE_CHIME_SCHEMA = vol.Schema({
vol.Required(ATTR_CODE): cv.string,
})
SERVICE_ALARM_TOGGLE_CHIME = "alarmdecoder_alarm_toggle_chime"
ALARM_TOGGLE_CHIME_SCHEMA = vol.Schema({vol.Required(ATTR_CODE): cv.string})
def setup_platform(hass, config, add_entities, discovery_info=None):
@ -30,8 +32,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
device.alarm_toggle_chime(code)
hass.services.register(
alarm.DOMAIN, SERVICE_ALARM_TOGGLE_CHIME, alarm_toggle_chime_handler,
schema=ALARM_TOGGLE_CHIME_SCHEMA)
alarm.DOMAIN,
SERVICE_ALARM_TOGGLE_CHIME,
alarm_toggle_chime_handler,
schema=ALARM_TOGGLE_CHIME_SCHEMA,
)
class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel):
@ -55,7 +60,8 @@ class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel):
async def async_added_to_hass(self):
"""Register callbacks."""
self.hass.helpers.dispatcher.async_dispatcher_connect(
SIGNAL_PANEL_MESSAGE, self._message_callback)
SIGNAL_PANEL_MESSAGE, self._message_callback
)
def _message_callback(self, message):
"""Handle received messages."""
@ -104,15 +110,15 @@ class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel):
def device_state_attributes(self):
"""Return the state attributes."""
return {
'ac_power': self._ac_power,
'backlight_on': self._backlight_on,
'battery_low': self._battery_low,
'check_zone': self._check_zone,
'chime': self._chime,
'entry_delay_off': self._entry_delay_off,
'programming_mode': self._programming_mode,
'ready': self._ready,
'zone_bypassed': self._zone_bypassed,
"ac_power": self._ac_power,
"backlight_on": self._backlight_on,
"battery_low": self._battery_low,
"check_zone": self._check_zone,
"chime": self._chime,
"entry_delay_off": self._entry_delay_off,
"programming_mode": self._programming_mode,
"ready": self._ready,
"zone_bypassed": self._zone_bypassed,
}
def alarm_disarm(self, code=None):

View file

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

View file

@ -24,13 +24,14 @@ class AlarmDecoderSensor(Entity):
"""Initialize the alarm panel."""
self._display = ""
self._state = None
self._icon = 'mdi:alarm-check'
self._name = 'Alarm Panel Display'
self._icon = "mdi:alarm-check"
self._name = "Alarm Panel Display"
async def async_added_to_hass(self):
"""Register callbacks."""
self.hass.helpers.dispatcher.async_dispatcher_connect(
SIGNAL_PANEL_MESSAGE, self._message_callback)
SIGNAL_PANEL_MESSAGE, self._message_callback
)
def _message_callback(self, message):
if self._display != message.text:

View file

@ -7,25 +7,32 @@ import voluptuous as vol
import homeassistant.components.alarm_control_panel as alarm
from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
from homeassistant.const import (
CONF_CODE, CONF_NAME, CONF_PASSWORD, CONF_USERNAME, STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED)
CONF_CODE,
CONF_NAME,
CONF_PASSWORD,
CONF_USERNAME,
STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME,
STATE_ALARM_DISARMED,
)
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'Alarm.com'
DEFAULT_NAME = "Alarm.com"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Optional(CONF_CODE): cv.positive_int,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Optional(CONF_CODE): cv.positive_int,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
}
)
async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up a Alarm.com control panel."""
name = config.get(CONF_NAME)
code = config.get(CONF_CODE)
@ -43,7 +50,8 @@ class AlarmDotCom(alarm.AlarmControlPanel):
def __init__(self, hass, name, code, username, password):
"""Initialize the Alarm.com status."""
from pyalarmdotcom import Alarmdotcom
_LOGGER.debug('Setting up Alarm.com...')
_LOGGER.debug("Setting up Alarm.com...")
self._hass = hass
self._name = name
self._code = str(code) if code else None
@ -51,8 +59,7 @@ class AlarmDotCom(alarm.AlarmControlPanel):
self._password = password
self._websession = async_get_clientsession(self._hass)
self._state = None
self._alarm = Alarmdotcom(
username, password, self._websession, hass.loop)
self._alarm = Alarmdotcom(username, password, self._websession, hass.loop)
async def async_login(self):
"""Login to Alarm.com."""
@ -73,27 +80,25 @@ class AlarmDotCom(alarm.AlarmControlPanel):
"""Return one or more digits/characters."""
if self._code is None:
return None
if isinstance(self._code, str) and re.search('^\\d+$', self._code):
if isinstance(self._code, str) and re.search("^\\d+$", self._code):
return alarm.FORMAT_NUMBER
return alarm.FORMAT_TEXT
@property
def state(self):
"""Return the state of the device."""
if self._alarm.state.lower() == 'disarmed':
if self._alarm.state.lower() == "disarmed":
return STATE_ALARM_DISARMED
if self._alarm.state.lower() == 'armed stay':
if self._alarm.state.lower() == "armed stay":
return STATE_ALARM_ARMED_HOME
if self._alarm.state.lower() == 'armed away':
if self._alarm.state.lower() == "armed away":
return STATE_ALARM_ARMED_AWAY
return None
@property
def device_state_attributes(self):
"""Return the state attributes."""
return {
'sensor_status': self._alarm.sensor_status
}
return {"sensor_status": self._alarm.sensor_status}
async def async_alarm_disarm(self, code=None):
"""Send disarm command."""

View file

@ -7,51 +7,65 @@ import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.notify import (
ATTR_MESSAGE, ATTR_TITLE, ATTR_DATA, DOMAIN as DOMAIN_NOTIFY)
ATTR_MESSAGE,
ATTR_TITLE,
ATTR_DATA,
DOMAIN as DOMAIN_NOTIFY,
)
from homeassistant.const import (
CONF_ENTITY_ID, STATE_IDLE, CONF_NAME, CONF_STATE, STATE_ON, STATE_OFF,
SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, ATTR_ENTITY_ID)
CONF_ENTITY_ID,
STATE_IDLE,
CONF_NAME,
CONF_STATE,
STATE_ON,
STATE_OFF,
SERVICE_TURN_ON,
SERVICE_TURN_OFF,
SERVICE_TOGGLE,
ATTR_ENTITY_ID,
)
from homeassistant.helpers import service, event
from homeassistant.helpers.entity import ToggleEntity
from homeassistant.util.dt import now
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'alert'
ENTITY_ID_FORMAT = DOMAIN + '.{}'
DOMAIN = "alert"
ENTITY_ID_FORMAT = DOMAIN + ".{}"
CONF_CAN_ACK = 'can_acknowledge'
CONF_NOTIFIERS = 'notifiers'
CONF_REPEAT = 'repeat'
CONF_SKIP_FIRST = 'skip_first'
CONF_ALERT_MESSAGE = 'message'
CONF_DONE_MESSAGE = 'done_message'
CONF_TITLE = 'title'
CONF_DATA = 'data'
CONF_CAN_ACK = "can_acknowledge"
CONF_NOTIFIERS = "notifiers"
CONF_REPEAT = "repeat"
CONF_SKIP_FIRST = "skip_first"
CONF_ALERT_MESSAGE = "message"
CONF_DONE_MESSAGE = "done_message"
CONF_TITLE = "title"
CONF_DATA = "data"
DEFAULT_CAN_ACK = True
DEFAULT_SKIP_FIRST = False
ALERT_SCHEMA = vol.Schema({
vol.Required(CONF_NAME): cv.string,
vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Required(CONF_STATE, default=STATE_ON): cv.string,
vol.Required(CONF_REPEAT): vol.All(cv.ensure_list, [vol.Coerce(float)]),
vol.Required(CONF_CAN_ACK, default=DEFAULT_CAN_ACK): cv.boolean,
vol.Required(CONF_SKIP_FIRST, default=DEFAULT_SKIP_FIRST): cv.boolean,
vol.Optional(CONF_ALERT_MESSAGE): cv.template,
vol.Optional(CONF_DONE_MESSAGE): cv.template,
vol.Optional(CONF_TITLE): cv.template,
vol.Optional(CONF_DATA): dict,
vol.Required(CONF_NOTIFIERS): cv.ensure_list})
ALERT_SCHEMA = vol.Schema(
{
vol.Required(CONF_NAME): cv.string,
vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Required(CONF_STATE, default=STATE_ON): cv.string,
vol.Required(CONF_REPEAT): vol.All(cv.ensure_list, [vol.Coerce(float)]),
vol.Required(CONF_CAN_ACK, default=DEFAULT_CAN_ACK): cv.boolean,
vol.Required(CONF_SKIP_FIRST, default=DEFAULT_SKIP_FIRST): cv.boolean,
vol.Optional(CONF_ALERT_MESSAGE): cv.template,
vol.Optional(CONF_DONE_MESSAGE): cv.template,
vol.Optional(CONF_TITLE): cv.template,
vol.Optional(CONF_DATA): dict,
vol.Required(CONF_NOTIFIERS): cv.ensure_list,
}
)
CONFIG_SCHEMA = vol.Schema({
DOMAIN: cv.schema_with_slug_keys(ALERT_SCHEMA),
}, extra=vol.ALLOW_EXTRA)
CONFIG_SCHEMA = vol.Schema(
{DOMAIN: cv.schema_with_slug_keys(ALERT_SCHEMA)}, extra=vol.ALLOW_EXTRA
)
ALERT_SERVICE_SCHEMA = vol.Schema({
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
})
ALERT_SERVICE_SCHEMA = vol.Schema({vol.Required(ATTR_ENTITY_ID): cv.entity_ids})
def is_on(hass, entity_id):
@ -79,11 +93,23 @@ async def async_setup(hass, config):
title_template = cfg.get(CONF_TITLE)
data = cfg.get(CONF_DATA)
entities.append(Alert(hass, object_id, name,
watched_entity_id, alert_state, repeat,
skip_first, message_template,
done_message_template, notifiers,
can_ack, title_template, data))
entities.append(
Alert(
hass,
object_id,
name,
watched_entity_id,
alert_state,
repeat,
skip_first,
message_template,
done_message_template,
notifiers,
can_ack,
title_template,
data,
)
)
if not entities:
return False
@ -107,14 +133,17 @@ async def async_setup(hass, config):
# Setup service calls
hass.services.async_register(
DOMAIN, SERVICE_TURN_OFF, async_handle_alert_service,
schema=ALERT_SERVICE_SCHEMA)
DOMAIN,
SERVICE_TURN_OFF,
async_handle_alert_service,
schema=ALERT_SERVICE_SCHEMA,
)
hass.services.async_register(
DOMAIN, SERVICE_TURN_ON, async_handle_alert_service,
schema=ALERT_SERVICE_SCHEMA)
DOMAIN, SERVICE_TURN_ON, async_handle_alert_service, schema=ALERT_SERVICE_SCHEMA
)
hass.services.async_register(
DOMAIN, SERVICE_TOGGLE, async_handle_alert_service,
schema=ALERT_SERVICE_SCHEMA)
DOMAIN, SERVICE_TOGGLE, async_handle_alert_service, schema=ALERT_SERVICE_SCHEMA
)
tasks = [alert.async_update_ha_state() for alert in entities]
if tasks:
@ -126,10 +155,22 @@ async def async_setup(hass, config):
class Alert(ToggleEntity):
"""Representation of an alert."""
def __init__(self, hass, entity_id, name, watched_entity_id,
state, repeat, skip_first, message_template,
done_message_template, notifiers, can_ack, title_template,
data):
def __init__(
self,
hass,
entity_id,
name,
watched_entity_id,
state,
repeat,
skip_first,
message_template,
done_message_template,
notifiers,
can_ack,
title_template,
data,
):
"""Initialize the alert."""
self.hass = hass
self._name = name
@ -162,7 +203,8 @@ class Alert(ToggleEntity):
self.entity_id = ENTITY_ID_FORMAT.format(entity_id)
event.async_track_state_change(
hass, watched_entity_id, self.watched_entity_change)
hass, watched_entity_id, self.watched_entity_change
)
@property
def name(self):
@ -224,8 +266,9 @@ class Alert(ToggleEntity):
"""Schedule a notification."""
delay = self._delay[self._next_delay]
next_msg = now() + delay
self._cancel = \
event.async_track_point_in_time(self.hass, self._notify, next_msg)
self._cancel = event.async_track_point_in_time(
self.hass, self._notify, next_msg
)
self._next_delay = min(self._next_delay + 1, len(self._delay) - 1)
async def _notify(self, *args):
@ -270,8 +313,7 @@ class Alert(ToggleEntity):
_LOGGER.debug(msg_payload)
for target in self._notifiers:
await self.hass.services.async_call(
DOMAIN_NOTIFY, target, msg_payload)
await self.hass.services.async_call(DOMAIN_NOTIFY, target, msg_payload)
async def async_turn_on(self, **kwargs):
"""Async Unacknowledge alert."""

View file

@ -9,45 +9,68 @@ from homeassistant.const import CONF_NAME
from . import flash_briefings, intent, smart_home_http
from .const import (
CONF_AUDIO, CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_DISPLAY_URL,
CONF_ENDPOINT, CONF_TEXT, CONF_TITLE, CONF_UID, DOMAIN, CONF_FILTER,
CONF_ENTITY_CONFIG, CONF_DESCRIPTION, CONF_DISPLAY_CATEGORIES)
CONF_AUDIO,
CONF_CLIENT_ID,
CONF_CLIENT_SECRET,
CONF_DISPLAY_URL,
CONF_ENDPOINT,
CONF_TEXT,
CONF_TITLE,
CONF_UID,
DOMAIN,
CONF_FILTER,
CONF_ENTITY_CONFIG,
CONF_DESCRIPTION,
CONF_DISPLAY_CATEGORIES,
)
_LOGGER = logging.getLogger(__name__)
CONF_FLASH_BRIEFINGS = 'flash_briefings'
CONF_SMART_HOME = 'smart_home'
CONF_FLASH_BRIEFINGS = "flash_briefings"
CONF_SMART_HOME = "smart_home"
ALEXA_ENTITY_SCHEMA = vol.Schema({
vol.Optional(CONF_DESCRIPTION): cv.string,
vol.Optional(CONF_DISPLAY_CATEGORIES): cv.string,
vol.Optional(CONF_NAME): cv.string,
})
SMART_HOME_SCHEMA = vol.Schema({
vol.Optional(CONF_ENDPOINT): cv.string,
vol.Optional(CONF_CLIENT_ID): cv.string,
vol.Optional(CONF_CLIENT_SECRET): cv.string,
vol.Optional(CONF_FILTER, default={}): entityfilter.FILTER_SCHEMA,
vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: ALEXA_ENTITY_SCHEMA}
})
CONFIG_SCHEMA = vol.Schema({
DOMAIN: {
CONF_FLASH_BRIEFINGS: {
cv.string: vol.All(cv.ensure_list, [{
vol.Optional(CONF_UID): cv.string,
vol.Required(CONF_TITLE): cv.template,
vol.Optional(CONF_AUDIO): cv.template,
vol.Required(CONF_TEXT, default=""): cv.template,
vol.Optional(CONF_DISPLAY_URL): cv.template,
}]),
},
# vol.Optional here would mean we couldn't distinguish between an empty
# smart_home: and none at all.
CONF_SMART_HOME: vol.Any(SMART_HOME_SCHEMA, None),
ALEXA_ENTITY_SCHEMA = vol.Schema(
{
vol.Optional(CONF_DESCRIPTION): cv.string,
vol.Optional(CONF_DISPLAY_CATEGORIES): cv.string,
vol.Optional(CONF_NAME): cv.string,
}
}, extra=vol.ALLOW_EXTRA)
)
SMART_HOME_SCHEMA = vol.Schema(
{
vol.Optional(CONF_ENDPOINT): cv.string,
vol.Optional(CONF_CLIENT_ID): cv.string,
vol.Optional(CONF_CLIENT_SECRET): cv.string,
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,
)
async def async_setup(hass, config):

View file

@ -13,12 +13,10 @@ from homeassistant.util import dt
_LOGGER = logging.getLogger(__name__)
LWA_TOKEN_URI = "https://api.amazon.com/auth/o2/token"
LWA_HEADERS = {
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8"
}
LWA_HEADERS = {"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8"}
PREEMPTIVE_REFRESH_TTL_IN_SECONDS = 300
STORAGE_KEY = 'alexa_auth'
STORAGE_KEY = "alexa_auth"
STORAGE_VERSION = 1
STORAGE_EXPIRE_TIME = "expire_time"
STORAGE_ACCESS_TOKEN = "access_token"
@ -49,10 +47,12 @@ class Auth:
"grant_type": "authorization_code",
"code": accept_grant_code,
"client_id": self.client_id,
"client_secret": self.client_secret
"client_secret": self.client_secret,
}
_LOGGER.debug("Calling LWA to get the access token (first time), "
"with: %s", json.dumps(lwa_params))
_LOGGER.debug(
"Calling LWA to get the access token (first time), " "with: %s",
json.dumps(lwa_params),
)
return await self._async_request_new_token(lwa_params)
@ -74,7 +74,7 @@ class Auth:
"grant_type": "refresh_token",
"refresh_token": self._prefs[STORAGE_REFRESH_TOKEN],
"client_id": self.client_id,
"client_secret": self.client_secret
"client_secret": self.client_secret,
}
_LOGGER.debug("Calling LWA to refresh the access token.")
@ -88,7 +88,8 @@ class Auth:
expire_time = dt.parse_datetime(self._prefs[STORAGE_EXPIRE_TIME])
preemptive_expire_time = expire_time - timedelta(
seconds=PREEMPTIVE_REFRESH_TTL_IN_SECONDS)
seconds=PREEMPTIVE_REFRESH_TTL_IN_SECONDS
)
return dt.utcnow() < preemptive_expire_time
@ -97,10 +98,12 @@ class Auth:
try:
session = aiohttp_client.async_get_clientsession(self.hass)
with async_timeout.timeout(10):
response = await session.post(LWA_TOKEN_URI,
headers=LWA_HEADERS,
data=lwa_params,
allow_redirects=True)
response = await session.post(
LWA_TOKEN_URI,
headers=LWA_HEADERS,
data=lwa_params,
allow_redirects=True,
)
except (asyncio.TimeoutError, aiohttp.ClientError):
_LOGGER.error("Timeout calling LWA to get auth token.")
@ -121,8 +124,9 @@ class Auth:
expires_in = response_json["expires_in"]
expire_time = dt.utcnow() + timedelta(seconds=expires_in)
await self._async_update_preferences(access_token, refresh_token,
expire_time.isoformat())
await self._async_update_preferences(
access_token, refresh_token, expire_time.isoformat()
)
return access_token
@ -134,11 +138,10 @@ class Auth:
self._prefs = {
STORAGE_ACCESS_TOKEN: None,
STORAGE_REFRESH_TOKEN: None,
STORAGE_EXPIRE_TIME: None
STORAGE_EXPIRE_TIME: None,
}
async def _async_update_preferences(self, access_token, refresh_token,
expire_time):
async def _async_update_preferences(self, access_token, refresh_token, expire_time):
"""Update user preferences."""
if self._prefs is None:
await self.async_load_preferences()

View file

@ -13,11 +13,7 @@ from homeassistant.const import (
STATE_UNLOCKED,
)
import homeassistant.components.climate.const as climate
from homeassistant.components import (
light,
fan,
cover,
)
from homeassistant.components import light, fan, cover
import homeassistant.util.color as color_util
from .const import (
@ -85,35 +81,35 @@ class AlexaCapibility:
def serialize_discovery(self):
"""Serialize according to the Discovery API."""
result = {
'type': 'AlexaInterface',
'interface': self.name(),
'version': '3',
'properties': {
'supported': self.properties_supported(),
'proactivelyReported': self.properties_proactively_reported(),
'retrievable': self.properties_retrievable(),
"type": "AlexaInterface",
"interface": self.name(),
"version": "3",
"properties": {
"supported": self.properties_supported(),
"proactivelyReported": self.properties_proactively_reported(),
"retrievable": self.properties_retrievable(),
},
}
# pylint: disable=assignment-from-none
supports_deactivation = self.supports_deactivation()
if supports_deactivation is not None:
result['supportsDeactivation'] = supports_deactivation
result["supportsDeactivation"] = supports_deactivation
return result
def serialize_properties(self):
"""Return properties serialized for an API response."""
for prop in self.properties_supported():
prop_name = prop['name']
prop_name = prop["name"]
# pylint: disable=assignment-from-no-return
prop_value = self.get_property(prop_name)
if prop_value is not None:
yield {
'name': prop_name,
'namespace': self.name(),
'value': prop_value,
'timeOfSample': datetime.now().strftime(DATE_FORMAT),
'uncertaintyInMilliseconds': 0
"name": prop_name,
"namespace": self.name(),
"value": prop_value,
"timeOfSample": datetime.now().strftime(DATE_FORMAT),
"uncertaintyInMilliseconds": 0,
}
@ -130,11 +126,11 @@ class AlexaEndpointHealth(AlexaCapibility):
def name(self):
"""Return the Alexa API name of this interface."""
return 'Alexa.EndpointHealth'
return "Alexa.EndpointHealth"
def properties_supported(self):
"""Return what properties this entity supports."""
return [{'name': 'connectivity'}]
return [{"name": "connectivity"}]
def properties_proactively_reported(self):
"""Return True if properties asynchronously reported."""
@ -146,12 +142,12 @@ class AlexaEndpointHealth(AlexaCapibility):
def get_property(self, name):
"""Read and return a property."""
if name != 'connectivity':
if name != "connectivity":
raise UnsupportedProperty(name)
if self.entity.state == STATE_UNAVAILABLE:
return {'value': 'UNREACHABLE'}
return {'value': 'OK'}
return {"value": "UNREACHABLE"}
return {"value": "OK"}
class AlexaPowerController(AlexaCapibility):
@ -162,11 +158,11 @@ class AlexaPowerController(AlexaCapibility):
def name(self):
"""Return the Alexa API name of this interface."""
return 'Alexa.PowerController'
return "Alexa.PowerController"
def properties_supported(self):
"""Return what properties this entity supports."""
return [{'name': 'powerState'}]
return [{"name": "powerState"}]
def properties_proactively_reported(self):
"""Return True if properties asynchronously reported."""
@ -178,7 +174,7 @@ class AlexaPowerController(AlexaCapibility):
def get_property(self, name):
"""Read and return a property."""
if name != 'powerState':
if name != "powerState":
raise UnsupportedProperty(name)
if self.entity.domain == climate.DOMAIN:
@ -187,7 +183,7 @@ class AlexaPowerController(AlexaCapibility):
else:
is_on = self.entity.state != STATE_OFF
return 'ON' if is_on else 'OFF'
return "ON" if is_on else "OFF"
class AlexaLockController(AlexaCapibility):
@ -198,11 +194,11 @@ class AlexaLockController(AlexaCapibility):
def name(self):
"""Return the Alexa API name of this interface."""
return 'Alexa.LockController'
return "Alexa.LockController"
def properties_supported(self):
"""Return what properties this entity supports."""
return [{'name': 'lockState'}]
return [{"name": "lockState"}]
def properties_retrievable(self):
"""Return True if properties can be retrieved."""
@ -214,14 +210,14 @@ class AlexaLockController(AlexaCapibility):
def get_property(self, name):
"""Read and return a property."""
if name != 'lockState':
if name != "lockState":
raise UnsupportedProperty(name)
if self.entity.state == STATE_LOCKED:
return 'LOCKED'
return "LOCKED"
if self.entity.state == STATE_UNLOCKED:
return 'UNLOCKED'
return 'JAMMED'
return "UNLOCKED"
return "JAMMED"
class AlexaSceneController(AlexaCapibility):
@ -237,7 +233,7 @@ class AlexaSceneController(AlexaCapibility):
def name(self):
"""Return the Alexa API name of this interface."""
return 'Alexa.SceneController'
return "Alexa.SceneController"
class AlexaBrightnessController(AlexaCapibility):
@ -248,11 +244,11 @@ class AlexaBrightnessController(AlexaCapibility):
def name(self):
"""Return the Alexa API name of this interface."""
return 'Alexa.BrightnessController'
return "Alexa.BrightnessController"
def properties_supported(self):
"""Return what properties this entity supports."""
return [{'name': 'brightness'}]
return [{"name": "brightness"}]
def properties_proactively_reported(self):
"""Return True if properties asynchronously reported."""
@ -264,10 +260,10 @@ class AlexaBrightnessController(AlexaCapibility):
def get_property(self, name):
"""Read and return a property."""
if name != 'brightness':
if name != "brightness":
raise UnsupportedProperty(name)
if 'brightness' in self.entity.attributes:
return round(self.entity.attributes['brightness'] / 255.0 * 100)
if "brightness" in self.entity.attributes:
return round(self.entity.attributes["brightness"] / 255.0 * 100)
return 0
@ -279,11 +275,11 @@ class AlexaColorController(AlexaCapibility):
def name(self):
"""Return the Alexa API name of this interface."""
return 'Alexa.ColorController'
return "Alexa.ColorController"
def properties_supported(self):
"""Return what properties this entity supports."""
return [{'name': 'color'}]
return [{"name": "color"}]
def properties_retrievable(self):
"""Return True if properties can be retrieved."""
@ -291,17 +287,15 @@ class AlexaColorController(AlexaCapibility):
def get_property(self, name):
"""Read and return a property."""
if name != 'color':
if name != "color":
raise UnsupportedProperty(name)
hue, saturation = self.entity.attributes.get(
light.ATTR_HS_COLOR, (0, 0))
hue, saturation = self.entity.attributes.get(light.ATTR_HS_COLOR, (0, 0))
return {
'hue': hue,
'saturation': saturation / 100.0,
'brightness': self.entity.attributes.get(
light.ATTR_BRIGHTNESS, 0) / 255.0,
"hue": hue,
"saturation": saturation / 100.0,
"brightness": self.entity.attributes.get(light.ATTR_BRIGHTNESS, 0) / 255.0,
}
@ -313,11 +307,11 @@ class AlexaColorTemperatureController(AlexaCapibility):
def name(self):
"""Return the Alexa API name of this interface."""
return 'Alexa.ColorTemperatureController'
return "Alexa.ColorTemperatureController"
def properties_supported(self):
"""Return what properties this entity supports."""
return [{'name': 'colorTemperatureInKelvin'}]
return [{"name": "colorTemperatureInKelvin"}]
def properties_retrievable(self):
"""Return True if properties can be retrieved."""
@ -325,11 +319,12 @@ class AlexaColorTemperatureController(AlexaCapibility):
def get_property(self, name):
"""Read and return a property."""
if name != 'colorTemperatureInKelvin':
if name != "colorTemperatureInKelvin":
raise UnsupportedProperty(name)
if 'color_temp' in self.entity.attributes:
if "color_temp" in self.entity.attributes:
return color_util.color_temperature_mired_to_kelvin(
self.entity.attributes['color_temp'])
self.entity.attributes["color_temp"]
)
return 0
@ -341,11 +336,11 @@ class AlexaPercentageController(AlexaCapibility):
def name(self):
"""Return the Alexa API name of this interface."""
return 'Alexa.PercentageController'
return "Alexa.PercentageController"
def properties_supported(self):
"""Return what properties this entity supports."""
return [{'name': 'percentage'}]
return [{"name": "percentage"}]
def properties_retrievable(self):
"""Return True if properties can be retrieved."""
@ -353,7 +348,7 @@ class AlexaPercentageController(AlexaCapibility):
def get_property(self, name):
"""Read and return a property."""
if name != 'percentage':
if name != "percentage":
raise UnsupportedProperty(name)
if self.entity.domain == fan.DOMAIN:
@ -375,7 +370,7 @@ class AlexaSpeaker(AlexaCapibility):
def name(self):
"""Return the Alexa API name of this interface."""
return 'Alexa.Speaker'
return "Alexa.Speaker"
class AlexaStepSpeaker(AlexaCapibility):
@ -386,7 +381,7 @@ class AlexaStepSpeaker(AlexaCapibility):
def name(self):
"""Return the Alexa API name of this interface."""
return 'Alexa.StepSpeaker'
return "Alexa.StepSpeaker"
class AlexaPlaybackController(AlexaCapibility):
@ -397,7 +392,7 @@ class AlexaPlaybackController(AlexaCapibility):
def name(self):
"""Return the Alexa API name of this interface."""
return 'Alexa.PlaybackController'
return "Alexa.PlaybackController"
class AlexaInputController(AlexaCapibility):
@ -408,7 +403,7 @@ class AlexaInputController(AlexaCapibility):
def name(self):
"""Return the Alexa API name of this interface."""
return 'Alexa.InputController'
return "Alexa.InputController"
class AlexaTemperatureSensor(AlexaCapibility):
@ -424,11 +419,11 @@ class AlexaTemperatureSensor(AlexaCapibility):
def name(self):
"""Return the Alexa API name of this interface."""
return 'Alexa.TemperatureSensor'
return "Alexa.TemperatureSensor"
def properties_supported(self):
"""Return what properties this entity supports."""
return [{'name': 'temperature'}]
return [{"name": "temperature"}]
def properties_proactively_reported(self):
"""Return True if properties asynchronously reported."""
@ -440,19 +435,15 @@ class AlexaTemperatureSensor(AlexaCapibility):
def get_property(self, name):
"""Read and return a property."""
if name != 'temperature':
if name != "temperature":
raise UnsupportedProperty(name)
unit = self.entity.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
temp = self.entity.state
if self.entity.domain == climate.DOMAIN:
unit = self.hass.config.units.temperature_unit
temp = self.entity.attributes.get(
climate.ATTR_CURRENT_TEMPERATURE)
return {
'value': float(temp),
'scale': API_TEMP_UNITS[unit],
}
temp = self.entity.attributes.get(climate.ATTR_CURRENT_TEMPERATURE)
return {"value": float(temp), "scale": API_TEMP_UNITS[unit]}
class AlexaContactSensor(AlexaCapibility):
@ -473,11 +464,11 @@ class AlexaContactSensor(AlexaCapibility):
def name(self):
"""Return the Alexa API name of this interface."""
return 'Alexa.ContactSensor'
return "Alexa.ContactSensor"
def properties_supported(self):
"""Return what properties this entity supports."""
return [{'name': 'detectionState'}]
return [{"name": "detectionState"}]
def properties_proactively_reported(self):
"""Return True if properties asynchronously reported."""
@ -489,12 +480,12 @@ class AlexaContactSensor(AlexaCapibility):
def get_property(self, name):
"""Read and return a property."""
if name != 'detectionState':
if name != "detectionState":
raise UnsupportedProperty(name)
if self.entity.state == STATE_ON:
return 'DETECTED'
return 'NOT_DETECTED'
return "DETECTED"
return "NOT_DETECTED"
class AlexaMotionSensor(AlexaCapibility):
@ -510,11 +501,11 @@ class AlexaMotionSensor(AlexaCapibility):
def name(self):
"""Return the Alexa API name of this interface."""
return 'Alexa.MotionSensor'
return "Alexa.MotionSensor"
def properties_supported(self):
"""Return what properties this entity supports."""
return [{'name': 'detectionState'}]
return [{"name": "detectionState"}]
def properties_proactively_reported(self):
"""Return True if properties asynchronously reported."""
@ -526,12 +517,12 @@ class AlexaMotionSensor(AlexaCapibility):
def get_property(self, name):
"""Read and return a property."""
if name != 'detectionState':
if name != "detectionState":
raise UnsupportedProperty(name)
if self.entity.state == STATE_ON:
return 'DETECTED'
return 'NOT_DETECTED'
return "DETECTED"
return "NOT_DETECTED"
class AlexaThermostatController(AlexaCapibility):
@ -547,17 +538,17 @@ class AlexaThermostatController(AlexaCapibility):
def name(self):
"""Return the Alexa API name of this interface."""
return 'Alexa.ThermostatController'
return "Alexa.ThermostatController"
def properties_supported(self):
"""Return what properties this entity supports."""
properties = [{'name': 'thermostatMode'}]
properties = [{"name": "thermostatMode"}]
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if supported & climate.SUPPORT_TARGET_TEMPERATURE:
properties.append({'name': 'targetSetpoint'})
properties.append({"name": "targetSetpoint"})
if supported & climate.SUPPORT_TARGET_TEMPERATURE_RANGE:
properties.append({'name': 'lowerSetpoint'})
properties.append({'name': 'upperSetpoint'})
properties.append({"name": "lowerSetpoint"})
properties.append({"name": "upperSetpoint"})
return properties
def properties_proactively_reported(self):
@ -570,7 +561,7 @@ class AlexaThermostatController(AlexaCapibility):
def get_property(self, name):
"""Read and return a property."""
if name == 'thermostatMode':
if name == "thermostatMode":
preset = self.entity.attributes.get(climate.ATTR_PRESET_MODE)
if preset in API_THERMOSTAT_PRESETS:
@ -580,17 +571,19 @@ class AlexaThermostatController(AlexaCapibility):
if mode is None:
_LOGGER.error(
"%s (%s) has unsupported state value '%s'",
self.entity.entity_id, type(self.entity),
self.entity.state)
self.entity.entity_id,
type(self.entity),
self.entity.state,
)
raise UnsupportedProperty(name)
return mode
unit = self.hass.config.units.temperature_unit
if name == 'targetSetpoint':
if name == "targetSetpoint":
temp = self.entity.attributes.get(ATTR_TEMPERATURE)
elif name == 'lowerSetpoint':
elif name == "lowerSetpoint":
temp = self.entity.attributes.get(climate.ATTR_TARGET_TEMP_LOW)
elif name == 'upperSetpoint':
elif name == "upperSetpoint":
temp = self.entity.attributes.get(climate.ATTR_TARGET_TEMP_HIGH)
else:
raise UnsupportedProperty(name)
@ -598,7 +591,4 @@ class AlexaThermostatController(AlexaCapibility):
if temp is None:
return None
return {
'value': float(temp),
'scale': API_TEMP_UNITS[unit],
}
return {"value": float(temp), "scale": API_TEMP_UNITS[unit]}

View file

@ -1,78 +1,68 @@
"""Constants for the Alexa integration."""
from collections import OrderedDict
from homeassistant.const import (
TEMP_CELSIUS,
TEMP_FAHRENHEIT,
)
from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT
from homeassistant.components.climate import const as climate
from homeassistant.components import fan
DOMAIN = 'alexa'
DOMAIN = "alexa"
# Flash briefing constants
CONF_UID = 'uid'
CONF_TITLE = 'title'
CONF_AUDIO = 'audio'
CONF_TEXT = 'text'
CONF_DISPLAY_URL = 'display_url'
CONF_UID = "uid"
CONF_TITLE = "title"
CONF_AUDIO = "audio"
CONF_TEXT = "text"
CONF_DISPLAY_URL = "display_url"
CONF_FILTER = 'filter'
CONF_ENTITY_CONFIG = 'entity_config'
CONF_ENDPOINT = 'endpoint'
CONF_CLIENT_ID = 'client_id'
CONF_CLIENT_SECRET = 'client_secret'
CONF_FILTER = "filter"
CONF_ENTITY_CONFIG = "entity_config"
CONF_ENDPOINT = "endpoint"
CONF_CLIENT_ID = "client_id"
CONF_CLIENT_SECRET = "client_secret"
ATTR_UID = 'uid'
ATTR_UPDATE_DATE = 'updateDate'
ATTR_TITLE_TEXT = 'titleText'
ATTR_STREAM_URL = 'streamUrl'
ATTR_MAIN_TEXT = 'mainText'
ATTR_REDIRECTION_URL = 'redirectionURL'
ATTR_UID = "uid"
ATTR_UPDATE_DATE = "updateDate"
ATTR_TITLE_TEXT = "titleText"
ATTR_STREAM_URL = "streamUrl"
ATTR_MAIN_TEXT = "mainText"
ATTR_REDIRECTION_URL = "redirectionURL"
SYN_RESOLUTION_MATCH = 'ER_SUCCESS_MATCH'
SYN_RESOLUTION_MATCH = "ER_SUCCESS_MATCH"
DATE_FORMAT = '%Y-%m-%dT%H:%M:%S.0Z'
DATE_FORMAT = "%Y-%m-%dT%H:%M:%S.0Z"
API_DIRECTIVE = 'directive'
API_ENDPOINT = 'endpoint'
API_EVENT = 'event'
API_CONTEXT = 'context'
API_HEADER = 'header'
API_PAYLOAD = 'payload'
API_SCOPE = 'scope'
API_CHANGE = 'change'
API_DIRECTIVE = "directive"
API_ENDPOINT = "endpoint"
API_EVENT = "event"
API_CONTEXT = "context"
API_HEADER = "header"
API_PAYLOAD = "payload"
API_SCOPE = "scope"
API_CHANGE = "change"
CONF_DESCRIPTION = 'description'
CONF_DISPLAY_CATEGORIES = 'display_categories'
CONF_DESCRIPTION = "description"
CONF_DISPLAY_CATEGORIES = "display_categories"
API_TEMP_UNITS = {
TEMP_FAHRENHEIT: 'FAHRENHEIT',
TEMP_CELSIUS: 'CELSIUS',
}
API_TEMP_UNITS = {TEMP_FAHRENHEIT: "FAHRENHEIT", TEMP_CELSIUS: "CELSIUS"}
# Needs to be ordered dict for `async_api_set_thermostat_mode` which does a
# reverse mapping of this dict and we want to map the first occurrance of OFF
# back to HA state.
API_THERMOSTAT_MODES = OrderedDict([
(climate.HVAC_MODE_HEAT, 'HEAT'),
(climate.HVAC_MODE_COOL, 'COOL'),
(climate.HVAC_MODE_HEAT_COOL, 'AUTO'),
(climate.HVAC_MODE_AUTO, 'AUTO'),
(climate.HVAC_MODE_OFF, 'OFF'),
(climate.HVAC_MODE_FAN_ONLY, 'OFF'),
(climate.HVAC_MODE_DRY, 'OFF'),
])
API_THERMOSTAT_PRESETS = {
climate.PRESET_ECO: 'ECO'
}
API_THERMOSTAT_MODES = OrderedDict(
[
(climate.HVAC_MODE_HEAT, "HEAT"),
(climate.HVAC_MODE_COOL, "COOL"),
(climate.HVAC_MODE_HEAT_COOL, "AUTO"),
(climate.HVAC_MODE_AUTO, "AUTO"),
(climate.HVAC_MODE_OFF, "OFF"),
(climate.HVAC_MODE_FAN_ONLY, "OFF"),
(climate.HVAC_MODE_DRY, "OFF"),
]
)
API_THERMOSTAT_PRESETS = {climate.PRESET_ECO: "ECO"}
PERCENTAGE_FAN_MAP = {
fan.SPEED_LOW: 33,
fan.SPEED_MEDIUM: 66,
fan.SPEED_HIGH: 100,
}
PERCENTAGE_FAN_MAP = {fan.SPEED_LOW: 33, fan.SPEED_MEDIUM: 66, fan.SPEED_HIGH: 100}
class Cause:
@ -84,25 +74,25 @@ class Cause:
# Indicates that the event was caused by a customer interaction with an
# application. For example, a customer switches on a light, or locks a door
# using the Alexa app or an app provided by a device vendor.
APP_INTERACTION = 'APP_INTERACTION'
APP_INTERACTION = "APP_INTERACTION"
# Indicates that the event was caused by a physical interaction with an
# endpoint. For example manually switching on a light or manually locking a
# door lock
PHYSICAL_INTERACTION = 'PHYSICAL_INTERACTION'
PHYSICAL_INTERACTION = "PHYSICAL_INTERACTION"
# Indicates that the event was caused by the periodic poll of an appliance,
# which found a change in value. For example, you might poll a temperature
# sensor every hour, and send the updated temperature to Alexa.
PERIODIC_POLL = 'PERIODIC_POLL'
PERIODIC_POLL = "PERIODIC_POLL"
# Indicates that the event was caused by the application of a device rule.
# For example, a customer configures a rule to switch on a light if a
# motion sensor detects motion. In this case, Alexa receives an event from
# the motion sensor, and another event from the light to indicate that its
# state change was caused by the rule.
RULE_TRIGGER = 'RULE_TRIGGER'
RULE_TRIGGER = "RULE_TRIGGER"
# Indicates that the event was caused by a voice interaction with Alexa.
# For example a user speaking to their Echo device.
VOICE_INTERACTION = 'VOICE_INTERACTION'
VOICE_INTERACTION = "VOICE_INTERACTION"

View file

@ -14,8 +14,21 @@ from homeassistant.const import (
from homeassistant.util.decorator import Registry
from homeassistant.components.climate import const as climate
from homeassistant.components import (
alert, automation, binary_sensor, cover, fan, group,
input_boolean, light, lock, media_player, scene, script, sensor, switch)
alert,
automation,
binary_sensor,
cover,
fan,
group,
input_boolean,
light,
lock,
media_player,
scene,
script,
sensor,
switch,
)
from .const import CONF_DESCRIPTION, CONF_DISPLAY_CATEGORIES
from .capabilities import (
@ -129,7 +142,7 @@ class AlexaEntity:
def alexa_id(self):
"""Return the Alexa API entity id."""
return self.entity.entity_id.replace('.', '#')
return self.entity.entity_id.replace(".", "#")
def display_categories(self):
"""Return a list of display categories."""
@ -171,15 +184,13 @@ class AlexaEntity:
def serialize_discovery(self):
"""Serialize the entity for discovery."""
return {
'displayCategories': self.display_categories(),
'cookie': {},
'endpointId': self.alexa_id(),
'friendlyName': self.friendly_name(),
'description': self.description(),
'manufacturerName': 'Home Assistant',
'capabilities': [
i.serialize_discovery() for i in self.interfaces()
]
"displayCategories": self.display_categories(),
"cookie": {},
"endpointId": self.alexa_id(),
"friendlyName": self.friendly_name(),
"description": self.description(),
"manufacturerName": "Home Assistant",
"capabilities": [i.serialize_discovery() for i in self.interfaces()],
}
@ -220,8 +231,10 @@ class GenericCapabilities(AlexaEntity):
def interfaces(self):
"""Yield the supported interfaces."""
return [AlexaPowerController(self.entity),
AlexaEndpointHealth(self.hass, self.entity)]
return [
AlexaPowerController(self.entity),
AlexaEndpointHealth(self.hass, self.entity),
]
@ENTITY_ADAPTERS.register(switch.DOMAIN)
@ -234,8 +247,10 @@ class SwitchCapabilities(AlexaEntity):
def interfaces(self):
"""Yield the supported interfaces."""
return [AlexaPowerController(self.entity),
AlexaEndpointHealth(self.hass, self.entity)]
return [
AlexaPowerController(self.entity),
AlexaEndpointHealth(self.hass, self.entity),
]
@ENTITY_ADAPTERS.register(climate.DOMAIN)
@ -249,8 +264,7 @@ class ClimateCapabilities(AlexaEntity):
def interfaces(self):
"""Yield the supported interfaces."""
# If we support two modes, one being off, we allow turning on too.
if (climate.HVAC_MODE_OFF in
self.entity.attributes[climate.ATTR_HVAC_MODES]):
if climate.HVAC_MODE_OFF in self.entity.attributes[climate.ATTR_HVAC_MODES]:
yield AlexaPowerController(self.entity)
yield AlexaThermostatController(self.hass, self.entity)
@ -324,8 +338,10 @@ class LockCapabilities(AlexaEntity):
def interfaces(self):
"""Yield the supported interfaces."""
return [AlexaLockController(self.entity),
AlexaEndpointHealth(self.hass, self.entity)]
return [
AlexaLockController(self.entity),
AlexaEndpointHealth(self.hass, self.entity),
]
@ENTITY_ADAPTERS.register(media_player.const.DOMAIN)
@ -345,16 +361,20 @@ class MediaPlayerCapabilities(AlexaEntity):
if supported & media_player.const.SUPPORT_VOLUME_SET:
yield AlexaSpeaker(self.entity)
step_volume_features = (media_player.const.SUPPORT_VOLUME_MUTE |
media_player.const.SUPPORT_VOLUME_STEP)
step_volume_features = (
media_player.const.SUPPORT_VOLUME_MUTE
| media_player.const.SUPPORT_VOLUME_STEP
)
if supported & step_volume_features:
yield AlexaStepSpeaker(self.entity)
playback_features = (media_player.const.SUPPORT_PLAY |
media_player.const.SUPPORT_PAUSE |
media_player.const.SUPPORT_STOP |
media_player.const.SUPPORT_NEXT_TRACK |
media_player.const.SUPPORT_PREVIOUS_TRACK)
playback_features = (
media_player.const.SUPPORT_PLAY
| media_player.const.SUPPORT_PAUSE
| media_player.const.SUPPORT_STOP
| media_player.const.SUPPORT_NEXT_TRACK
| media_player.const.SUPPORT_PREVIOUS_TRACK
)
if supported & playback_features:
yield AlexaPlaybackController(self.entity)
@ -369,7 +389,7 @@ class SceneCapabilities(AlexaEntity):
def description(self):
"""Return the description of the entity."""
# Required description as per Amazon Scene docs
scene_fmt = '{} (Scene connected via Home Assistant)'
scene_fmt = "{} (Scene connected via Home Assistant)"
return scene_fmt.format(AlexaEntity.description(self))
def default_display_categories(self):
@ -378,8 +398,7 @@ class SceneCapabilities(AlexaEntity):
def interfaces(self):
"""Yield the supported interfaces."""
return [AlexaSceneController(self.entity,
supports_deactivation=False)]
return [AlexaSceneController(self.entity, supports_deactivation=False)]
@ENTITY_ADAPTERS.register(script.DOMAIN)
@ -392,9 +411,8 @@ class ScriptCapabilities(AlexaEntity):
def interfaces(self):
"""Yield the supported interfaces."""
can_cancel = bool(self.entity.attributes.get('can_cancel'))
return [AlexaSceneController(self.entity,
supports_deactivation=can_cancel)]
can_cancel = bool(self.entity.attributes.get("can_cancel"))
return [AlexaSceneController(self.entity, supports_deactivation=can_cancel)]
@ENTITY_ADAPTERS.register(sensor.DOMAIN)
@ -410,10 +428,7 @@ class SensorCapabilities(AlexaEntity):
def interfaces(self):
"""Yield the supported interfaces."""
attrs = self.entity.attributes
if attrs.get(ATTR_UNIT_OF_MEASUREMENT) in (
TEMP_FAHRENHEIT,
TEMP_CELSIUS,
):
if attrs.get(ATTR_UNIT_OF_MEASUREMENT) in (TEMP_FAHRENHEIT, TEMP_CELSIUS):
yield AlexaTemperatureSensor(self.hass, self.entity)
yield AlexaEndpointHealth(self.hass, self.entity)
@ -422,8 +437,8 @@ class SensorCapabilities(AlexaEntity):
class BinarySensorCapabilities(AlexaEntity):
"""Class to represent BinarySensor capabilities."""
TYPE_CONTACT = 'contact'
TYPE_MOTION = 'motion'
TYPE_CONTACT = "contact"
TYPE_MOTION = "motion"
def default_display_categories(self):
"""Return the display categories for this entity."""
@ -446,12 +461,7 @@ class BinarySensorCapabilities(AlexaEntity):
def get_type(self):
"""Return the type of binary sensor."""
attrs = self.entity.attributes
if attrs.get(ATTR_DEVICE_CLASS) in (
'door',
'garage_door',
'opening',
'window',
):
if attrs.get(ATTR_DEVICE_CLASS) in ("door", "garage_door", "opening", "window"):
return self.TYPE_CONTACT
if attrs.get(ATTR_DEVICE_CLASS) == 'motion':
if attrs.get(ATTR_DEVICE_CLASS) == "motion":
return self.TYPE_MOTION

View file

@ -35,12 +35,12 @@ class AlexaError(Exception):
class AlexaInvalidEndpointError(AlexaError):
"""The endpoint in the request does not exist."""
namespace = 'Alexa'
error_type = 'NO_SUCH_ENDPOINT'
namespace = "Alexa"
error_type = "NO_SUCH_ENDPOINT"
def __init__(self, endpoint_id):
"""Initialize invalid endpoint error."""
msg = 'The endpoint {} does not exist'.format(endpoint_id)
msg = "The endpoint {} does not exist".format(endpoint_id)
AlexaError.__init__(self, msg)
self.endpoint_id = endpoint_id
@ -48,38 +48,32 @@ class AlexaInvalidEndpointError(AlexaError):
class AlexaInvalidValueError(AlexaError):
"""Class to represent InvalidValue errors."""
namespace = 'Alexa'
error_type = 'INVALID_VALUE'
namespace = "Alexa"
error_type = "INVALID_VALUE"
class AlexaUnsupportedThermostatModeError(AlexaError):
"""Class to represent UnsupportedThermostatMode errors."""
namespace = 'Alexa.ThermostatController'
error_type = 'UNSUPPORTED_THERMOSTAT_MODE'
namespace = "Alexa.ThermostatController"
error_type = "UNSUPPORTED_THERMOSTAT_MODE"
class AlexaTempRangeError(AlexaError):
"""Class to represent TempRange errors."""
namespace = 'Alexa'
error_type = 'TEMPERATURE_VALUE_OUT_OF_RANGE'
namespace = "Alexa"
error_type = "TEMPERATURE_VALUE_OUT_OF_RANGE"
def __init__(self, hass, temp, min_temp, max_temp):
"""Initialize TempRange error."""
unit = hass.config.units.temperature_unit
temp_range = {
'minimumValue': {
'value': min_temp,
'scale': API_TEMP_UNITS[unit],
},
'maximumValue': {
'value': max_temp,
'scale': API_TEMP_UNITS[unit],
},
"minimumValue": {"value": min_temp, "scale": API_TEMP_UNITS[unit]},
"maximumValue": {"value": max_temp, "scale": API_TEMP_UNITS[unit]},
}
payload = {'validRange': temp_range}
msg = 'The requested temperature {} is out of range'.format(temp)
payload = {"validRange": temp_range}
msg = "The requested temperature {} is out of range".format(temp)
AlexaError.__init__(self, msg, payload)
@ -87,5 +81,5 @@ class AlexaTempRangeError(AlexaError):
class AlexaBridgeUnreachableError(AlexaError):
"""Class to represent BridgeUnreachable errors."""
namespace = 'Alexa'
error_type = 'BRIDGE_UNREACHABLE'
namespace = "Alexa"
error_type = "BRIDGE_UNREACHABLE"

View file

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

View file

@ -7,29 +7,44 @@ from homeassistant import core as ha
from homeassistant.components import cover, fan, group, light, media_player
from homeassistant.components.climate import const as climate
from homeassistant.const import (
ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, SERVICE_LOCK,
SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY,
SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP,
SERVICE_SET_COVER_POSITION, SERVICE_TURN_OFF, SERVICE_TURN_ON,
SERVICE_UNLOCK, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_MUTE,
SERVICE_VOLUME_SET, SERVICE_VOLUME_UP, TEMP_CELSIUS, TEMP_FAHRENHEIT)
ATTR_ENTITY_ID,
ATTR_SUPPORTED_FEATURES,
ATTR_TEMPERATURE,
SERVICE_LOCK,
SERVICE_MEDIA_NEXT_TRACK,
SERVICE_MEDIA_PAUSE,
SERVICE_MEDIA_PLAY,
SERVICE_MEDIA_PREVIOUS_TRACK,
SERVICE_MEDIA_STOP,
SERVICE_SET_COVER_POSITION,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
SERVICE_UNLOCK,
SERVICE_VOLUME_DOWN,
SERVICE_VOLUME_MUTE,
SERVICE_VOLUME_SET,
SERVICE_VOLUME_UP,
TEMP_CELSIUS,
TEMP_FAHRENHEIT,
)
import homeassistant.util.color as color_util
from homeassistant.util.decorator import Registry
from homeassistant.util.temperature import convert as convert_temperature
from .const import (
API_TEMP_UNITS, API_THERMOSTAT_MODES, API_THERMOSTAT_PRESETS, Cause)
from .const import API_TEMP_UNITS, API_THERMOSTAT_MODES, API_THERMOSTAT_PRESETS, Cause
from .entities import async_get_entities
from .errors import (
AlexaInvalidValueError, AlexaTempRangeError,
AlexaUnsupportedThermostatModeError)
AlexaInvalidValueError,
AlexaTempRangeError,
AlexaUnsupportedThermostatModeError,
)
from .state_report import async_enable_proactive_mode
_LOGGER = logging.getLogger(__name__)
HANDLERS = Registry()
@HANDLERS.register(('Alexa.Discovery', 'Discover'))
@HANDLERS.register(("Alexa.Discovery", "Discover"))
async def async_api_discovery(hass, config, directive, context):
"""Create a API formatted discovery response.
@ -42,19 +57,19 @@ async def async_api_discovery(hass, config, directive, context):
]
return directive.response(
name='Discover.Response',
namespace='Alexa.Discovery',
payload={'endpoints': discovery_endpoints},
name="Discover.Response",
namespace="Alexa.Discovery",
payload={"endpoints": discovery_endpoints},
)
@HANDLERS.register(('Alexa.Authorization', 'AcceptGrant'))
@HANDLERS.register(("Alexa.Authorization", "AcceptGrant"))
async def async_api_accept_grant(hass, config, directive, context):
"""Create a API formatted AcceptGrant response.
Async friendly.
"""
auth_code = directive.payload['grant']['code']
auth_code = directive.payload["grant"]["code"]
_LOGGER.debug("AcceptGrant code: %s", auth_code)
if config.supports_auth:
@ -64,12 +79,11 @@ async def async_api_accept_grant(hass, config, directive, context):
await async_enable_proactive_mode(hass, config)
return directive.response(
name='AcceptGrant.Response',
namespace='Alexa.Authorization',
payload={})
name="AcceptGrant.Response", namespace="Alexa.Authorization", payload={}
)
@HANDLERS.register(('Alexa.PowerController', 'TurnOn'))
@HANDLERS.register(("Alexa.PowerController", "TurnOn"))
async def async_api_turn_on(hass, config, directive, context):
"""Process a turn on request."""
entity = directive.entity
@ -82,19 +96,22 @@ async def async_api_turn_on(hass, config, directive, context):
service = cover.SERVICE_OPEN_COVER
elif domain == media_player.DOMAIN:
supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
power_features = (media_player.SUPPORT_TURN_ON |
media_player.SUPPORT_TURN_OFF)
power_features = media_player.SUPPORT_TURN_ON | media_player.SUPPORT_TURN_OFF
if not supported & power_features:
service = media_player.SERVICE_MEDIA_PLAY
await hass.services.async_call(domain, service, {
ATTR_ENTITY_ID: entity.entity_id
}, blocking=False, context=context)
await hass.services.async_call(
domain,
service,
{ATTR_ENTITY_ID: entity.entity_id},
blocking=False,
context=context,
)
return directive.response()
@HANDLERS.register(('Alexa.PowerController', 'TurnOff'))
@HANDLERS.register(("Alexa.PowerController", "TurnOff"))
async def async_api_turn_off(hass, config, directive, context):
"""Process a turn off request."""
entity = directive.entity
@ -107,89 +124,104 @@ async def async_api_turn_off(hass, config, directive, context):
service = cover.SERVICE_CLOSE_COVER
elif domain == media_player.DOMAIN:
supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
power_features = (media_player.SUPPORT_TURN_ON |
media_player.SUPPORT_TURN_OFF)
power_features = media_player.SUPPORT_TURN_ON | media_player.SUPPORT_TURN_OFF
if not supported & power_features:
service = media_player.SERVICE_MEDIA_STOP
await hass.services.async_call(domain, service, {
ATTR_ENTITY_ID: entity.entity_id
}, blocking=False, context=context)
await hass.services.async_call(
domain,
service,
{ATTR_ENTITY_ID: entity.entity_id},
blocking=False,
context=context,
)
return directive.response()
@HANDLERS.register(('Alexa.BrightnessController', 'SetBrightness'))
@HANDLERS.register(("Alexa.BrightnessController", "SetBrightness"))
async def async_api_set_brightness(hass, config, directive, context):
"""Process a set brightness request."""
entity = directive.entity
brightness = int(directive.payload['brightness'])
brightness = int(directive.payload["brightness"])
await hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: entity.entity_id,
light.ATTR_BRIGHTNESS_PCT: brightness,
}, blocking=False, context=context)
await hass.services.async_call(
entity.domain,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: entity.entity_id, light.ATTR_BRIGHTNESS_PCT: brightness},
blocking=False,
context=context,
)
return directive.response()
@HANDLERS.register(('Alexa.BrightnessController', 'AdjustBrightness'))
@HANDLERS.register(("Alexa.BrightnessController", "AdjustBrightness"))
async def async_api_adjust_brightness(hass, config, directive, context):
"""Process an adjust brightness request."""
entity = directive.entity
brightness_delta = int(directive.payload['brightnessDelta'])
brightness_delta = int(directive.payload["brightnessDelta"])
# read current state
try:
current = math.floor(
int(entity.attributes.get(light.ATTR_BRIGHTNESS)) / 255 * 100)
int(entity.attributes.get(light.ATTR_BRIGHTNESS)) / 255 * 100
)
except ZeroDivisionError:
current = 0
# set brightness
brightness = max(0, brightness_delta + current)
await hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: entity.entity_id,
light.ATTR_BRIGHTNESS_PCT: brightness,
}, blocking=False, context=context)
await hass.services.async_call(
entity.domain,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: entity.entity_id, light.ATTR_BRIGHTNESS_PCT: brightness},
blocking=False,
context=context,
)
return directive.response()
@HANDLERS.register(('Alexa.ColorController', 'SetColor'))
@HANDLERS.register(("Alexa.ColorController", "SetColor"))
async def async_api_set_color(hass, config, directive, context):
"""Process a set color request."""
entity = directive.entity
rgb = color_util.color_hsb_to_RGB(
float(directive.payload['color']['hue']),
float(directive.payload['color']['saturation']),
float(directive.payload['color']['brightness'])
float(directive.payload["color"]["hue"]),
float(directive.payload["color"]["saturation"]),
float(directive.payload["color"]["brightness"]),
)
await hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: entity.entity_id,
light.ATTR_RGB_COLOR: rgb,
}, blocking=False, context=context)
await hass.services.async_call(
entity.domain,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: entity.entity_id, light.ATTR_RGB_COLOR: rgb},
blocking=False,
context=context,
)
return directive.response()
@HANDLERS.register(('Alexa.ColorTemperatureController', 'SetColorTemperature'))
@HANDLERS.register(("Alexa.ColorTemperatureController", "SetColorTemperature"))
async def async_api_set_color_temperature(hass, config, directive, context):
"""Process a set color temperature request."""
entity = directive.entity
kelvin = int(directive.payload['colorTemperatureInKelvin'])
kelvin = int(directive.payload["colorTemperatureInKelvin"])
await hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: entity.entity_id,
light.ATTR_KELVIN: kelvin,
}, blocking=False, context=context)
await hass.services.async_call(
entity.domain,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: entity.entity_id, light.ATTR_KELVIN: kelvin},
blocking=False,
context=context,
)
return directive.response()
@HANDLERS.register(
('Alexa.ColorTemperatureController', 'DecreaseColorTemperature'))
@HANDLERS.register(("Alexa.ColorTemperatureController", "DecreaseColorTemperature"))
async def async_api_decrease_color_temp(hass, config, directive, context):
"""Process a decrease color temperature request."""
entity = directive.entity
@ -197,16 +229,18 @@ async def async_api_decrease_color_temp(hass, config, directive, context):
max_mireds = int(entity.attributes.get(light.ATTR_MAX_MIREDS))
value = min(max_mireds, current + 50)
await hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: entity.entity_id,
light.ATTR_COLOR_TEMP: value,
}, blocking=False, context=context)
await hass.services.async_call(
entity.domain,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: entity.entity_id, light.ATTR_COLOR_TEMP: value},
blocking=False,
context=context,
)
return directive.response()
@HANDLERS.register(
('Alexa.ColorTemperatureController', 'IncreaseColorTemperature'))
@HANDLERS.register(("Alexa.ColorTemperatureController", "IncreaseColorTemperature"))
async def async_api_increase_color_temp(hass, config, directive, context):
"""Process an increase color temperature request."""
entity = directive.entity
@ -214,63 +248,70 @@ async def async_api_increase_color_temp(hass, config, directive, context):
min_mireds = int(entity.attributes.get(light.ATTR_MIN_MIREDS))
value = max(min_mireds, current - 50)
await hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: entity.entity_id,
light.ATTR_COLOR_TEMP: value,
}, blocking=False, context=context)
await hass.services.async_call(
entity.domain,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: entity.entity_id, light.ATTR_COLOR_TEMP: value},
blocking=False,
context=context,
)
return directive.response()
@HANDLERS.register(('Alexa.SceneController', 'Activate'))
@HANDLERS.register(("Alexa.SceneController", "Activate"))
async def async_api_activate(hass, config, directive, context):
"""Process an activate request."""
entity = directive.entity
domain = entity.domain
await hass.services.async_call(domain, SERVICE_TURN_ON, {
ATTR_ENTITY_ID: entity.entity_id
}, blocking=False, context=context)
await hass.services.async_call(
domain,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: entity.entity_id},
blocking=False,
context=context,
)
payload = {
'cause': {'type': Cause.VOICE_INTERACTION},
'timestamp': '%sZ' % (datetime.utcnow().isoformat(),)
"cause": {"type": Cause.VOICE_INTERACTION},
"timestamp": "%sZ" % (datetime.utcnow().isoformat(),),
}
return directive.response(
name='ActivationStarted',
namespace='Alexa.SceneController',
payload=payload,
name="ActivationStarted", namespace="Alexa.SceneController", payload=payload
)
@HANDLERS.register(('Alexa.SceneController', 'Deactivate'))
@HANDLERS.register(("Alexa.SceneController", "Deactivate"))
async def async_api_deactivate(hass, config, directive, context):
"""Process a deactivate request."""
entity = directive.entity
domain = entity.domain
await hass.services.async_call(domain, SERVICE_TURN_OFF, {
ATTR_ENTITY_ID: entity.entity_id
}, blocking=False, context=context)
await hass.services.async_call(
domain,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: entity.entity_id},
blocking=False,
context=context,
)
payload = {
'cause': {'type': Cause.VOICE_INTERACTION},
'timestamp': '%sZ' % (datetime.utcnow().isoformat(),)
"cause": {"type": Cause.VOICE_INTERACTION},
"timestamp": "%sZ" % (datetime.utcnow().isoformat(),),
}
return directive.response(
name='DeactivationStarted',
namespace='Alexa.SceneController',
payload=payload,
name="DeactivationStarted", namespace="Alexa.SceneController", payload=payload
)
@HANDLERS.register(('Alexa.PercentageController', 'SetPercentage'))
@HANDLERS.register(("Alexa.PercentageController", "SetPercentage"))
async def async_api_set_percentage(hass, config, directive, context):
"""Process a set percentage request."""
entity = directive.entity
percentage = int(directive.payload['percentage'])
percentage = int(directive.payload["percentage"])
service = None
data = {ATTR_ENTITY_ID: entity.entity_id}
@ -291,16 +332,17 @@ async def async_api_set_percentage(hass, config, directive, context):
data[cover.ATTR_POSITION] = percentage
await hass.services.async_call(
entity.domain, service, data, blocking=False, context=context)
entity.domain, service, data, blocking=False, context=context
)
return directive.response()
@HANDLERS.register(('Alexa.PercentageController', 'AdjustPercentage'))
@HANDLERS.register(("Alexa.PercentageController", "AdjustPercentage"))
async def async_api_adjust_percentage(hass, config, directive, context):
"""Process an adjust percentage request."""
entity = directive.entity
percentage_delta = int(directive.payload['percentageDelta'])
percentage_delta = int(directive.payload["percentageDelta"])
service = None
data = {ATTR_ENTITY_ID: entity.entity_id}
@ -338,44 +380,51 @@ async def async_api_adjust_percentage(hass, config, directive, context):
data[cover.ATTR_POSITION] = max(0, percentage_delta + current)
await hass.services.async_call(
entity.domain, service, data, blocking=False, context=context)
entity.domain, service, data, blocking=False, context=context
)
return directive.response()
@HANDLERS.register(('Alexa.LockController', 'Lock'))
@HANDLERS.register(("Alexa.LockController", "Lock"))
async def async_api_lock(hass, config, directive, context):
"""Process a lock request."""
entity = directive.entity
await hass.services.async_call(entity.domain, SERVICE_LOCK, {
ATTR_ENTITY_ID: entity.entity_id
}, blocking=False, context=context)
await hass.services.async_call(
entity.domain,
SERVICE_LOCK,
{ATTR_ENTITY_ID: entity.entity_id},
blocking=False,
context=context,
)
response = directive.response()
response.add_context_property({
'name': 'lockState',
'namespace': 'Alexa.LockController',
'value': 'LOCKED'
})
response.add_context_property(
{"name": "lockState", "namespace": "Alexa.LockController", "value": "LOCKED"}
)
return response
# Not supported by Alexa yet
@HANDLERS.register(('Alexa.LockController', 'Unlock'))
@HANDLERS.register(("Alexa.LockController", "Unlock"))
async def async_api_unlock(hass, config, directive, context):
"""Process an unlock request."""
entity = directive.entity
await hass.services.async_call(entity.domain, SERVICE_UNLOCK, {
ATTR_ENTITY_ID: entity.entity_id
}, blocking=False, context=context)
await hass.services.async_call(
entity.domain,
SERVICE_UNLOCK,
{ATTR_ENTITY_ID: entity.entity_id},
blocking=False,
context=context,
)
return directive.response()
@HANDLERS.register(('Alexa.Speaker', 'SetVolume'))
@HANDLERS.register(("Alexa.Speaker", "SetVolume"))
async def async_api_set_volume(hass, config, directive, context):
"""Process a set volume request."""
volume = round(float(directive.payload['volume'] / 100), 2)
volume = round(float(directive.payload["volume"] / 100), 2)
entity = directive.entity
data = {
@ -384,31 +433,31 @@ async def async_api_set_volume(hass, config, directive, context):
}
await hass.services.async_call(
entity.domain, SERVICE_VOLUME_SET,
data, blocking=False, context=context)
entity.domain, SERVICE_VOLUME_SET, data, blocking=False, context=context
)
return directive.response()
@HANDLERS.register(('Alexa.InputController', 'SelectInput'))
@HANDLERS.register(("Alexa.InputController", "SelectInput"))
async def async_api_select_input(hass, config, directive, context):
"""Process a set input request."""
media_input = directive.payload['input']
media_input = directive.payload["input"]
entity = directive.entity
# attempt to map the ALL UPPERCASE payload name to a source
source_list = entity.attributes[
media_player.const.ATTR_INPUT_SOURCE_LIST] or []
source_list = entity.attributes[media_player.const.ATTR_INPUT_SOURCE_LIST] or []
for source in source_list:
# response will always be space separated, so format the source in the
# most likely way to find a match
formatted_source = source.lower().replace('-', ' ').replace('_', ' ')
formatted_source = source.lower().replace("-", " ").replace("_", " ")
if formatted_source in media_input.lower():
media_input = source
break
else:
msg = 'failed to map input {} to a media source on {}'.format(
media_input, entity.entity_id)
msg = "failed to map input {} to a media source on {}".format(
media_input, entity.entity_id
)
raise AlexaInvalidValueError(msg)
data = {
@ -417,20 +466,23 @@ async def async_api_select_input(hass, config, directive, context):
}
await hass.services.async_call(
entity.domain, media_player.SERVICE_SELECT_SOURCE,
data, blocking=False, context=context)
entity.domain,
media_player.SERVICE_SELECT_SOURCE,
data,
blocking=False,
context=context,
)
return directive.response()
@HANDLERS.register(('Alexa.Speaker', 'AdjustVolume'))
@HANDLERS.register(("Alexa.Speaker", "AdjustVolume"))
async def async_api_adjust_volume(hass, config, directive, context):
"""Process an adjust volume request."""
volume_delta = int(directive.payload['volume'])
volume_delta = int(directive.payload["volume"])
entity = directive.entity
current_level = entity.attributes.get(
media_player.const.ATTR_MEDIA_VOLUME_LEVEL)
current_level = entity.attributes.get(media_player.const.ATTR_MEDIA_VOLUME_LEVEL)
# read current state
try:
@ -446,43 +498,41 @@ async def async_api_adjust_volume(hass, config, directive, context):
}
await hass.services.async_call(
entity.domain, SERVICE_VOLUME_SET,
data, blocking=False, context=context)
entity.domain, SERVICE_VOLUME_SET, data, blocking=False, context=context
)
return directive.response()
@HANDLERS.register(('Alexa.StepSpeaker', 'AdjustVolume'))
@HANDLERS.register(("Alexa.StepSpeaker", "AdjustVolume"))
async def async_api_adjust_volume_step(hass, config, directive, context):
"""Process an adjust volume step request."""
# media_player volume up/down service does not support specifying steps
# each component handles it differently e.g. via config.
# For now we use the volumeSteps returned to figure out if we
# should step up/down
volume_step = directive.payload['volumeSteps']
volume_step = directive.payload["volumeSteps"]
entity = directive.entity
data = {
ATTR_ENTITY_ID: entity.entity_id,
}
data = {ATTR_ENTITY_ID: entity.entity_id}
if volume_step > 0:
await hass.services.async_call(
entity.domain, SERVICE_VOLUME_UP,
data, blocking=False, context=context)
entity.domain, SERVICE_VOLUME_UP, data, blocking=False, context=context
)
elif volume_step < 0:
await hass.services.async_call(
entity.domain, SERVICE_VOLUME_DOWN,
data, blocking=False, context=context)
entity.domain, SERVICE_VOLUME_DOWN, data, blocking=False, context=context
)
return directive.response()
@HANDLERS.register(('Alexa.StepSpeaker', 'SetMute'))
@HANDLERS.register(('Alexa.Speaker', 'SetMute'))
@HANDLERS.register(("Alexa.StepSpeaker", "SetMute"))
@HANDLERS.register(("Alexa.Speaker", "SetMute"))
async def async_api_set_mute(hass, config, directive, context):
"""Process a set mute request."""
mute = bool(directive.payload['mute'])
mute = bool(directive.payload["mute"])
entity = directive.entity
data = {
@ -491,83 +541,77 @@ async def async_api_set_mute(hass, config, directive, context):
}
await hass.services.async_call(
entity.domain, SERVICE_VOLUME_MUTE,
data, blocking=False, context=context)
entity.domain, SERVICE_VOLUME_MUTE, data, blocking=False, context=context
)
return directive.response()
@HANDLERS.register(('Alexa.PlaybackController', 'Play'))
@HANDLERS.register(("Alexa.PlaybackController", "Play"))
async def async_api_play(hass, config, directive, context):
"""Process a play request."""
entity = directive.entity
data = {
ATTR_ENTITY_ID: entity.entity_id
}
data = {ATTR_ENTITY_ID: entity.entity_id}
await hass.services.async_call(
entity.domain, SERVICE_MEDIA_PLAY,
data, blocking=False, context=context)
entity.domain, SERVICE_MEDIA_PLAY, data, blocking=False, context=context
)
return directive.response()
@HANDLERS.register(('Alexa.PlaybackController', 'Pause'))
@HANDLERS.register(("Alexa.PlaybackController", "Pause"))
async def async_api_pause(hass, config, directive, context):
"""Process a pause request."""
entity = directive.entity
data = {
ATTR_ENTITY_ID: entity.entity_id
}
data = {ATTR_ENTITY_ID: entity.entity_id}
await hass.services.async_call(
entity.domain, SERVICE_MEDIA_PAUSE,
data, blocking=False, context=context)
entity.domain, SERVICE_MEDIA_PAUSE, data, blocking=False, context=context
)
return directive.response()
@HANDLERS.register(('Alexa.PlaybackController', 'Stop'))
@HANDLERS.register(("Alexa.PlaybackController", "Stop"))
async def async_api_stop(hass, config, directive, context):
"""Process a stop request."""
entity = directive.entity
data = {
ATTR_ENTITY_ID: entity.entity_id
}
data = {ATTR_ENTITY_ID: entity.entity_id}
await hass.services.async_call(
entity.domain, SERVICE_MEDIA_STOP,
data, blocking=False, context=context)
entity.domain, SERVICE_MEDIA_STOP, data, blocking=False, context=context
)
return directive.response()
@HANDLERS.register(('Alexa.PlaybackController', 'Next'))
@HANDLERS.register(("Alexa.PlaybackController", "Next"))
async def async_api_next(hass, config, directive, context):
"""Process a next request."""
entity = directive.entity
data = {
ATTR_ENTITY_ID: entity.entity_id
}
data = {ATTR_ENTITY_ID: entity.entity_id}
await hass.services.async_call(
entity.domain, SERVICE_MEDIA_NEXT_TRACK,
data, blocking=False, context=context)
entity.domain, SERVICE_MEDIA_NEXT_TRACK, data, blocking=False, context=context
)
return directive.response()
@HANDLERS.register(('Alexa.PlaybackController', 'Previous'))
@HANDLERS.register(("Alexa.PlaybackController", "Previous"))
async def async_api_previous(hass, config, directive, context):
"""Process a previous request."""
entity = directive.entity
data = {
ATTR_ENTITY_ID: entity.entity_id
}
data = {ATTR_ENTITY_ID: entity.entity_id}
await hass.services.async_call(
entity.domain, SERVICE_MEDIA_PREVIOUS_TRACK,
data, blocking=False, context=context)
entity.domain,
SERVICE_MEDIA_PREVIOUS_TRACK,
data,
blocking=False,
context=context,
)
return directive.response()
@ -576,11 +620,11 @@ def temperature_from_object(hass, temp_obj, interval=False):
"""Get temperature from Temperature object in requested unit."""
to_unit = hass.config.units.temperature_unit
from_unit = TEMP_CELSIUS
temp = float(temp_obj['value'])
temp = float(temp_obj["value"])
if temp_obj['scale'] == 'FAHRENHEIT':
if temp_obj["scale"] == "FAHRENHEIT":
from_unit = TEMP_FAHRENHEIT
elif temp_obj['scale'] == 'KELVIN':
elif temp_obj["scale"] == "KELVIN":
# convert to Celsius if absolute temperature
if not interval:
temp -= 273.15
@ -588,7 +632,7 @@ def temperature_from_object(hass, temp_obj, interval=False):
return convert_temperature(temp, from_unit, to_unit, interval)
@HANDLERS.register(('Alexa.ThermostatController', 'SetTargetTemperature'))
@HANDLERS.register(("Alexa.ThermostatController", "SetTargetTemperature"))
async def async_api_set_target_temp(hass, config, directive, context):
"""Process a set target temperature request."""
entity = directive.entity
@ -596,51 +640,59 @@ async def async_api_set_target_temp(hass, config, directive, context):
max_temp = entity.attributes.get(climate.ATTR_MAX_TEMP)
unit = hass.config.units.temperature_unit
data = {
ATTR_ENTITY_ID: entity.entity_id
}
data = {ATTR_ENTITY_ID: entity.entity_id}
payload = directive.payload
response = directive.response()
if 'targetSetpoint' in payload:
temp = temperature_from_object(hass, payload['targetSetpoint'])
if "targetSetpoint" in payload:
temp = temperature_from_object(hass, payload["targetSetpoint"])
if temp < min_temp or temp > max_temp:
raise AlexaTempRangeError(hass, temp, min_temp, max_temp)
data[ATTR_TEMPERATURE] = temp
response.add_context_property({
'name': 'targetSetpoint',
'namespace': 'Alexa.ThermostatController',
'value': {'value': temp, 'scale': API_TEMP_UNITS[unit]},
})
if 'lowerSetpoint' in payload:
temp_low = temperature_from_object(hass, payload['lowerSetpoint'])
response.add_context_property(
{
"name": "targetSetpoint",
"namespace": "Alexa.ThermostatController",
"value": {"value": temp, "scale": API_TEMP_UNITS[unit]},
}
)
if "lowerSetpoint" in payload:
temp_low = temperature_from_object(hass, payload["lowerSetpoint"])
if temp_low < min_temp or temp_low > max_temp:
raise AlexaTempRangeError(hass, temp_low, min_temp, max_temp)
data[climate.ATTR_TARGET_TEMP_LOW] = temp_low
response.add_context_property({
'name': 'lowerSetpoint',
'namespace': 'Alexa.ThermostatController',
'value': {'value': temp_low, 'scale': API_TEMP_UNITS[unit]},
})
if 'upperSetpoint' in payload:
temp_high = temperature_from_object(hass, payload['upperSetpoint'])
response.add_context_property(
{
"name": "lowerSetpoint",
"namespace": "Alexa.ThermostatController",
"value": {"value": temp_low, "scale": API_TEMP_UNITS[unit]},
}
)
if "upperSetpoint" in payload:
temp_high = temperature_from_object(hass, payload["upperSetpoint"])
if temp_high < min_temp or temp_high > max_temp:
raise AlexaTempRangeError(hass, temp_high, min_temp, max_temp)
data[climate.ATTR_TARGET_TEMP_HIGH] = temp_high
response.add_context_property({
'name': 'upperSetpoint',
'namespace': 'Alexa.ThermostatController',
'value': {'value': temp_high, 'scale': API_TEMP_UNITS[unit]},
})
response.add_context_property(
{
"name": "upperSetpoint",
"namespace": "Alexa.ThermostatController",
"value": {"value": temp_high, "scale": API_TEMP_UNITS[unit]},
}
)
await hass.services.async_call(
entity.domain, climate.SERVICE_SET_TEMPERATURE, data, blocking=False,
context=context)
entity.domain,
climate.SERVICE_SET_TEMPERATURE,
data,
blocking=False,
context=context,
)
return response
@HANDLERS.register(('Alexa.ThermostatController', 'AdjustTargetTemperature'))
@HANDLERS.register(("Alexa.ThermostatController", "AdjustTargetTemperature"))
async def async_api_adjust_target_temp(hass, config, directive, context):
"""Process an adjust target temperature request."""
entity = directive.entity
@ -649,53 +701,50 @@ async def async_api_adjust_target_temp(hass, config, directive, context):
unit = hass.config.units.temperature_unit
temp_delta = temperature_from_object(
hass, directive.payload['targetSetpointDelta'], interval=True)
hass, directive.payload["targetSetpointDelta"], interval=True
)
target_temp = float(entity.attributes.get(ATTR_TEMPERATURE)) + temp_delta
if target_temp < min_temp or target_temp > max_temp:
raise AlexaTempRangeError(hass, target_temp, min_temp, max_temp)
data = {
ATTR_ENTITY_ID: entity.entity_id,
ATTR_TEMPERATURE: target_temp,
}
data = {ATTR_ENTITY_ID: entity.entity_id, ATTR_TEMPERATURE: target_temp}
response = directive.response()
await hass.services.async_call(
entity.domain, climate.SERVICE_SET_TEMPERATURE, data, blocking=False,
context=context)
response.add_context_property({
'name': 'targetSetpoint',
'namespace': 'Alexa.ThermostatController',
'value': {'value': target_temp, 'scale': API_TEMP_UNITS[unit]},
})
entity.domain,
climate.SERVICE_SET_TEMPERATURE,
data,
blocking=False,
context=context,
)
response.add_context_property(
{
"name": "targetSetpoint",
"namespace": "Alexa.ThermostatController",
"value": {"value": target_temp, "scale": API_TEMP_UNITS[unit]},
}
)
return response
@HANDLERS.register(('Alexa.ThermostatController', 'SetThermostatMode'))
@HANDLERS.register(("Alexa.ThermostatController", "SetThermostatMode"))
async def async_api_set_thermostat_mode(hass, config, directive, context):
"""Process a set thermostat mode request."""
entity = directive.entity
mode = directive.payload['thermostatMode']
mode = mode if isinstance(mode, str) else mode['value']
mode = directive.payload["thermostatMode"]
mode = mode if isinstance(mode, str) else mode["value"]
data = {
ATTR_ENTITY_ID: entity.entity_id,
}
data = {ATTR_ENTITY_ID: entity.entity_id}
ha_preset = next(
(k for k, v in API_THERMOSTAT_PRESETS.items() if v == mode),
None
)
ha_preset = next((k for k, v in API_THERMOSTAT_PRESETS.items() if v == mode), None)
if ha_preset:
presets = entity.attributes.get(climate.ATTR_PRESET_MODES, [])
if ha_preset not in presets:
msg = 'The requested thermostat mode {} is not supported'.format(
ha_preset
)
msg = "The requested thermostat mode {} is not supported".format(ha_preset)
raise AlexaUnsupportedThermostatModeError(msg)
service = climate.SERVICE_SET_PRESET_MODE
@ -703,14 +752,9 @@ async def async_api_set_thermostat_mode(hass, config, directive, context):
else:
operation_list = entity.attributes.get(climate.ATTR_HVAC_MODES)
ha_mode = next(
(k for k, v in API_THERMOSTAT_MODES.items() if v == mode),
None
)
ha_mode = next((k for k, v in API_THERMOSTAT_MODES.items() if v == mode), None)
if ha_mode not in operation_list:
msg = 'The requested thermostat mode {} is not supported'.format(
mode
)
msg = "The requested thermostat mode {} is not supported".format(mode)
raise AlexaUnsupportedThermostatModeError(msg)
service = climate.SERVICE_SET_HVAC_MODE
@ -718,18 +762,20 @@ async def async_api_set_thermostat_mode(hass, config, directive, context):
response = directive.response()
await hass.services.async_call(
climate.DOMAIN, service, data,
blocking=False, context=context)
response.add_context_property({
'name': 'thermostatMode',
'namespace': 'Alexa.ThermostatController',
'value': mode,
})
climate.DOMAIN, service, data, blocking=False, context=context
)
response.add_context_property(
{
"name": "thermostatMode",
"namespace": "Alexa.ThermostatController",
"value": mode,
}
)
return response
@HANDLERS.register(('Alexa', 'ReportState'))
@HANDLERS.register(("Alexa", "ReportState"))
async def async_api_reportstate(hass, config, directive, context):
"""Process a ReportState request."""
return directive.response(name='StateReport')
return directive.response(name="StateReport")

View file

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

View file

@ -23,8 +23,8 @@ class AlexaDirective:
def __init__(self, request):
"""Initialize a directive."""
self._directive = request[API_DIRECTIVE]
self.namespace = self._directive[API_HEADER]['namespace']
self.name = self._directive[API_HEADER]['name']
self.namespace = self._directive[API_HEADER]["namespace"]
self.name = self._directive[API_HEADER]["name"]
self.payload = self._directive[API_PAYLOAD]
self.has_endpoint = API_ENDPOINT in self._directive
@ -44,27 +44,23 @@ class AlexaDirective:
Will raise AlexaInvalidEndpointError if the endpoint in the request is
malformed or nonexistant.
"""
_endpoint_id = self._directive[API_ENDPOINT]['endpointId']
self.entity_id = _endpoint_id.replace('#', '.')
_endpoint_id = self._directive[API_ENDPOINT]["endpointId"]
self.entity_id = _endpoint_id.replace("#", ".")
self.entity = hass.states.get(self.entity_id)
if not self.entity or not config.should_expose(self.entity_id):
raise AlexaInvalidEndpointError(_endpoint_id)
self.endpoint = ENTITY_ADAPTERS[self.entity.domain](
hass, config, self.entity)
self.endpoint = ENTITY_ADAPTERS[self.entity.domain](hass, config, self.entity)
def response(self,
name='Response',
namespace='Alexa',
payload=None):
def response(self, name="Response", namespace="Alexa", payload=None):
"""Create an API formatted response.
Async friendly.
"""
response = AlexaResponse(name, namespace, payload)
token = self._directive[API_HEADER].get('correlationToken')
token = self._directive[API_HEADER].get("correlationToken")
if token:
response.set_correlation_token(token)
@ -74,31 +70,30 @@ class AlexaDirective:
return response
def error(
self,
namespace='Alexa',
error_type='INTERNAL_ERROR',
error_message="",
payload=None
self,
namespace="Alexa",
error_type="INTERNAL_ERROR",
error_message="",
payload=None,
):
"""Create a API formatted error response.
Async friendly.
"""
payload = payload or {}
payload['type'] = error_type
payload['message'] = error_message
payload["type"] = error_type
payload["message"] = error_message
_LOGGER.info("Request %s/%s error %s: %s",
self._directive[API_HEADER]['namespace'],
self._directive[API_HEADER]['name'],
error_type, error_message)
return self.response(
name='ErrorResponse',
namespace=namespace,
payload=payload
_LOGGER.info(
"Request %s/%s error %s: %s",
self._directive[API_HEADER]["namespace"],
self._directive[API_HEADER]["name"],
error_type,
error_message,
)
return self.response(name="ErrorResponse", namespace=namespace, payload=payload)
class AlexaResponse:
"""Class to hold a response."""
@ -109,10 +104,10 @@ class AlexaResponse:
self._response = {
API_EVENT: {
API_HEADER: {
'namespace': namespace,
'name': name,
'messageId': str(uuid4()),
'payloadVersion': '3',
"namespace": namespace,
"name": name,
"messageId": str(uuid4()),
"payloadVersion": "3",
},
API_PAYLOAD: payload,
}
@ -121,12 +116,12 @@ class AlexaResponse:
@property
def name(self):
"""Return the name of this response."""
return self._response[API_EVENT][API_HEADER]['name']
return self._response[API_EVENT][API_HEADER]["name"]
@property
def namespace(self):
"""Return the namespace of this response."""
return self._response[API_EVENT][API_HEADER]['namespace']
return self._response[API_EVENT][API_HEADER]["namespace"]
def set_correlation_token(self, token):
"""Set the correlationToken.
@ -134,7 +129,7 @@ class AlexaResponse:
This should normally mirror the value from a request, and is set by
AlexaDirective.response() usually.
"""
self._response[API_EVENT][API_HEADER]['correlationToken'] = token
self._response[API_EVENT][API_HEADER]["correlationToken"] = token
def set_endpoint_full(self, bearer_token, endpoint_id, cookie=None):
"""Set the endpoint dictionary.
@ -142,17 +137,14 @@ class AlexaResponse:
This is used to send proactive messages to Alexa.
"""
self._response[API_EVENT][API_ENDPOINT] = {
API_SCOPE: {
'type': 'BearerToken',
'token': bearer_token
}
API_SCOPE: {"type": "BearerToken", "token": bearer_token}
}
if endpoint_id is not None:
self._response[API_EVENT][API_ENDPOINT]['endpointId'] = endpoint_id
self._response[API_EVENT][API_ENDPOINT]["endpointId"] = endpoint_id
if cookie is not None:
self._response[API_EVENT][API_ENDPOINT]['cookie'] = cookie
self._response[API_EVENT][API_ENDPOINT]["cookie"] = cookie
def set_endpoint(self, endpoint):
"""Set the endpoint.
@ -164,7 +156,7 @@ class AlexaResponse:
def _properties(self):
context = self._response.setdefault(API_CONTEXT, {})
return context.setdefault('properties', [])
return context.setdefault("properties", [])
def add_context_property(self, prop):
"""Add a property to the response context.
@ -189,10 +181,10 @@ class AlexaResponse:
Handlers should be using .add_context_property().
"""
properties = self._properties()
already_set = {(p['namespace'], p['name']) for p in properties}
already_set = {(p["namespace"], p["name"]) for p in properties}
for prop in endpoint.serialize_properties():
if (prop['namespace'], prop['name']) not in already_set:
if (prop["namespace"], prop["name"]) not in already_set:
self.add_context_property(prop)
def serialize(self):

View file

@ -4,32 +4,23 @@ import logging
import homeassistant.core as ha
from .const import API_DIRECTIVE, API_HEADER
from .errors import (
AlexaError,
AlexaBridgeUnreachableError,
)
from .errors import AlexaError, AlexaBridgeUnreachableError
from .handlers import HANDLERS
from .messages import AlexaDirective
_LOGGER = logging.getLogger(__name__)
EVENT_ALEXA_SMART_HOME = 'alexa_smart_home'
EVENT_ALEXA_SMART_HOME = "alexa_smart_home"
async def async_handle_message(
hass,
config,
request,
context=None,
enabled=True,
):
async def async_handle_message(hass, config, request, context=None, enabled=True):
"""Handle incoming API messages.
If enabled is False, the response to all messagess will be a
BRIDGE_UNREACHABLE error. This can be used if the API has been disabled in
configuration.
"""
assert request[API_DIRECTIVE][API_HEADER]['payloadVersion'] == '3'
assert request[API_DIRECTIVE][API_HEADER]["payloadVersion"] == "3"
if context is None:
context = ha.Context()
@ -39,7 +30,8 @@ async def async_handle_message(
try:
if not enabled:
raise AlexaBridgeUnreachableError(
'Alexa API not enabled in Home Assistant configuration')
"Alexa API not enabled in Home Assistant configuration"
)
if directive.has_endpoint:
directive.load_entity(hass, config)
@ -51,30 +43,26 @@ async def async_handle_message(
response.merge_context_properties(directive.endpoint)
else:
_LOGGER.warning(
"Unsupported API request %s/%s",
directive.namespace,
directive.name,
"Unsupported API request %s/%s", directive.namespace, directive.name
)
response = directive.error()
except AlexaError as err:
response = directive.error(
error_type=err.error_type,
error_message=err.error_message)
error_type=err.error_type, error_message=err.error_message
)
request_info = {
'namespace': directive.namespace,
'name': directive.name,
}
request_info = {"namespace": directive.namespace, "name": directive.name}
if directive.has_endpoint:
request_info['entity_id'] = directive.entity_id
request_info["entity_id"] = directive.entity_id
hass.bus.async_fire(EVENT_ALEXA_SMART_HOME, {
'request': request_info,
'response': {
'namespace': response.namespace,
'name': response.name,
}
}, context=context)
hass.bus.async_fire(
EVENT_ALEXA_SMART_HOME,
{
"request": request_info,
"response": {"namespace": response.namespace, "name": response.name},
},
context=context,
)
return response.serialize()

View file

@ -11,13 +11,13 @@ from .const import (
CONF_CLIENT_SECRET,
CONF_ENDPOINT,
CONF_ENTITY_CONFIG,
CONF_FILTER
CONF_FILTER,
)
from .state_report import async_enable_proactive_mode
from .smart_home import async_handle_message
_LOGGER = logging.getLogger(__name__)
SMART_HOME_HTTP_ENDPOINT = '/api/alexa/smart_home'
SMART_HOME_HTTP_ENDPOINT = "/api/alexa/smart_home"
class AlexaConfig(AbstractConfig):
@ -29,8 +29,7 @@ class AlexaConfig(AbstractConfig):
self._config = config
if config.get(CONF_CLIENT_ID) and config.get(CONF_CLIENT_SECRET):
self._auth = Auth(hass, config[CONF_CLIENT_ID],
config[CONF_CLIENT_SECRET])
self._auth = Auth(hass, config[CONF_CLIENT_ID], config[CONF_CLIENT_SECRET])
else:
self._auth = None
@ -87,7 +86,7 @@ class SmartHomeView(HomeAssistantView):
"""Expose Smart Home v3 payload interface via HTTP POST."""
url = SMART_HOME_HTTP_ENDPOINT
name = 'api:alexa:smart_home'
name = "api:alexa:smart_home"
def __init__(self, smart_home_config):
"""Initialize."""
@ -100,15 +99,14 @@ class SmartHomeView(HomeAssistantView):
Lambda, which will need to forward the requests to here and pass back
the response.
"""
hass = request.app['hass']
user = request['hass_user']
hass = request.app["hass"]
user = request["hass_user"]
message = await request.json()
_LOGGER.debug("Received Alexa Smart Home request: %s", message)
response = await async_handle_message(
hass, self.smart_home_config, message,
context=core.Context(user_id=user.id)
hass, self.smart_home_config, message, context=core.Context(user_id=user.id)
)
_LOGGER.debug("Sending Alexa Smart Home response: %s", response)
return b'' if response is None else self.json(response)
return b"" if response is None else self.json(response)

View file

@ -24,8 +24,7 @@ async def async_enable_proactive_mode(hass, smart_home_config):
# Validate we can get access token.
await smart_home_config.async_get_access_token()
async def async_entity_state_listener(changed_entity, old_state,
new_state):
async def async_entity_state_listener(changed_entity, old_state, new_state):
if not new_state:
return
@ -33,18 +32,18 @@ async def async_enable_proactive_mode(hass, smart_home_config):
return
if not smart_home_config.should_expose(changed_entity):
_LOGGER.debug("Not exposing %s because filtered by config",
changed_entity)
_LOGGER.debug("Not exposing %s because filtered by config", changed_entity)
return
alexa_changed_entity = \
ENTITY_ADAPTERS[new_state.domain](hass, smart_home_config,
new_state)
alexa_changed_entity = ENTITY_ADAPTERS[new_state.domain](
hass, smart_home_config, new_state
)
for interface in alexa_changed_entity.interfaces():
if interface.properties_proactively_reported():
await async_send_changereport_message(hass, smart_home_config,
alexa_changed_entity)
await async_send_changereport_message(
hass, smart_home_config, alexa_changed_entity
)
return
return hass.helpers.event.async_track_state_change(
@ -59,9 +58,7 @@ async def async_send_changereport_message(hass, config, alexa_entity):
"""
token = await config.async_get_access_token()
headers = {
"Authorization": "Bearer {}".format(token)
}
headers = {"Authorization": "Bearer {}".format(token)}
endpoint = alexa_entity.alexa_id()
@ -71,14 +68,10 @@ async def async_send_changereport_message(hass, config, alexa_entity):
properties = list(alexa_entity.serialize_properties())
payload = {
API_CHANGE: {
'cause': {'type': Cause.APP_INTERACTION},
'properties': properties
}
API_CHANGE: {"cause": {"type": Cause.APP_INTERACTION}, "properties": properties}
}
message = AlexaResponse(name='ChangeReport', namespace='Alexa',
payload=payload)
message = AlexaResponse(name="ChangeReport", namespace="Alexa", payload=payload)
message.set_endpoint_full(token, endpoint)
message_serialized = message.serialize()
@ -86,10 +79,12 @@ async def async_send_changereport_message(hass, config, alexa_entity):
try:
with async_timeout.timeout(DEFAULT_TIMEOUT):
response = await session.post(config.endpoint,
headers=headers,
json=message_serialized,
allow_redirects=True)
response = await session.post(
config.endpoint,
headers=headers,
json=message_serialized,
allow_redirects=True,
)
except (asyncio.TimeoutError, aiohttp.ClientError):
_LOGGER.error("Timeout sending report to Alexa.")
@ -102,9 +97,11 @@ async def async_send_changereport_message(hass, config, alexa_entity):
if response.status != 202:
response_json = json.loads(response_text)
_LOGGER.error("Error when sending ChangeReport to Alexa: %s: %s",
response_json["payload"]["code"],
response_json["payload"]["description"])
_LOGGER.error(
"Error when sending ChangeReport to Alexa: %s: %s",
response_json["payload"]["code"],
response_json["payload"]["description"],
)
async def async_send_add_or_update_message(hass, config, entity_ids):
@ -114,35 +111,27 @@ async def async_send_add_or_update_message(hass, config, entity_ids):
"""
token = await config.async_get_access_token()
headers = {
"Authorization": "Bearer {}".format(token)
}
headers = {"Authorization": "Bearer {}".format(token)}
endpoints = []
for entity_id in entity_ids:
domain = entity_id.split('.', 1)[0]
alexa_entity = ENTITY_ADAPTERS[domain](
hass, config, hass.states.get(entity_id)
)
domain = entity_id.split(".", 1)[0]
alexa_entity = ENTITY_ADAPTERS[domain](hass, config, hass.states.get(entity_id))
endpoints.append(alexa_entity.serialize_discovery())
payload = {
'endpoints': endpoints,
'scope': {
'type': 'BearerToken',
'token': token,
}
}
payload = {"endpoints": endpoints, "scope": {"type": "BearerToken", "token": token}}
message = AlexaResponse(
name='AddOrUpdateReport', namespace='Alexa.Discovery', payload=payload)
name="AddOrUpdateReport", namespace="Alexa.Discovery", payload=payload
)
message_serialized = message.serialize()
session = hass.helpers.aiohttp_client.async_get_clientsession()
return await session.post(config.endpoint, headers=headers,
json=message_serialized, allow_redirects=True)
return await session.post(
config.endpoint, headers=headers, json=message_serialized, allow_redirects=True
)
async def async_send_delete_message(hass, config, entity_ids):
@ -152,34 +141,24 @@ async def async_send_delete_message(hass, config, entity_ids):
"""
token = await config.async_get_access_token()
headers = {
"Authorization": "Bearer {}".format(token)
}
headers = {"Authorization": "Bearer {}".format(token)}
endpoints = []
for entity_id in entity_ids:
domain = entity_id.split('.', 1)[0]
alexa_entity = ENTITY_ADAPTERS[domain](
hass, config, hass.states.get(entity_id)
)
endpoints.append({
'endpointId': alexa_entity.alexa_id()
})
domain = entity_id.split(".", 1)[0]
alexa_entity = ENTITY_ADAPTERS[domain](hass, config, hass.states.get(entity_id))
endpoints.append({"endpointId": alexa_entity.alexa_id()})
payload = {
'endpoints': endpoints,
'scope': {
'type': 'BearerToken',
'token': token,
}
}
payload = {"endpoints": endpoints, "scope": {"type": "BearerToken", "token": token}}
message = AlexaResponse(name='DeleteReport', namespace='Alexa.Discovery',
payload=payload)
message = AlexaResponse(
name="DeleteReport", namespace="Alexa.Discovery", payload=payload
)
message_serialized = message.serialize()
session = hass.helpers.aiohttp_client.async_get_clientsession()
return await session.post(config.endpoint, headers=headers,
json=message_serialized, allow_redirects=True)
return await session.post(
config.endpoint, headers=headers, json=message_serialized, allow_redirects=True
)

View file

@ -5,56 +5,59 @@ import logging
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (
ATTR_ATTRIBUTION, CONF_API_KEY, CONF_CURRENCY, CONF_NAME)
from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_CURRENCY, CONF_NAME
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
ATTR_CLOSE = 'close'
ATTR_HIGH = 'high'
ATTR_LOW = 'low'
ATTR_CLOSE = "close"
ATTR_HIGH = "high"
ATTR_LOW = "low"
ATTRIBUTION = "Stock market information provided by Alpha Vantage"
CONF_FOREIGN_EXCHANGE = 'foreign_exchange'
CONF_FROM = 'from'
CONF_SYMBOL = 'symbol'
CONF_SYMBOLS = 'symbols'
CONF_TO = 'to'
CONF_FOREIGN_EXCHANGE = "foreign_exchange"
CONF_FROM = "from"
CONF_SYMBOL = "symbol"
CONF_SYMBOLS = "symbols"
CONF_TO = "to"
ICONS = {
'BTC': 'mdi:currency-btc',
'EUR': 'mdi:currency-eur',
'GBP': 'mdi:currency-gbp',
'INR': 'mdi:currency-inr',
'RUB': 'mdi:currency-rub',
'TRY': 'mdi:currency-try',
'USD': 'mdi:currency-usd',
"BTC": "mdi:currency-btc",
"EUR": "mdi:currency-eur",
"GBP": "mdi:currency-gbp",
"INR": "mdi:currency-inr",
"RUB": "mdi:currency-rub",
"TRY": "mdi:currency-try",
"USD": "mdi:currency-usd",
}
SCAN_INTERVAL = timedelta(minutes=5)
SYMBOL_SCHEMA = vol.Schema({
vol.Required(CONF_SYMBOL): cv.string,
vol.Optional(CONF_CURRENCY): cv.string,
vol.Optional(CONF_NAME): cv.string,
})
SYMBOL_SCHEMA = vol.Schema(
{
vol.Required(CONF_SYMBOL): cv.string,
vol.Optional(CONF_CURRENCY): cv.string,
vol.Optional(CONF_NAME): cv.string,
}
)
CURRENCY_SCHEMA = vol.Schema({
vol.Required(CONF_FROM): cv.string,
vol.Required(CONF_TO): cv.string,
vol.Optional(CONF_NAME): cv.string,
})
CURRENCY_SCHEMA = vol.Schema(
{
vol.Required(CONF_FROM): cv.string,
vol.Required(CONF_TO): cv.string,
vol.Optional(CONF_NAME): cv.string,
}
)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_API_KEY): cv.string,
vol.Optional(CONF_FOREIGN_EXCHANGE):
vol.All(cv.ensure_list, [CURRENCY_SCHEMA]),
vol.Optional(CONF_SYMBOLS):
vol.All(cv.ensure_list, [SYMBOL_SCHEMA]),
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_API_KEY): cv.string,
vol.Optional(CONF_FOREIGN_EXCHANGE): vol.All(cv.ensure_list, [CURRENCY_SCHEMA]),
vol.Optional(CONF_SYMBOLS): vol.All(cv.ensure_list, [SYMBOL_SCHEMA]),
}
)
def setup_platform(hass, config, add_entities, discovery_info=None):
@ -67,9 +70,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
conversions = config.get(CONF_FOREIGN_EXCHANGE, [])
if not symbols and not conversions:
msg = 'Warning: No symbols or currencies configured.'
hass.components.persistent_notification.create(
msg, 'Sensor alpha_vantage')
msg = "Warning: No symbols or currencies configured."
hass.components.persistent_notification.create(msg, "Sensor alpha_vantage")
_LOGGER.warning(msg)
return
@ -78,12 +80,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
dev = []
for symbol in symbols:
try:
_LOGGER.debug("Configuring timeseries for symbols: %s",
symbol[CONF_SYMBOL])
_LOGGER.debug("Configuring timeseries for symbols: %s", symbol[CONF_SYMBOL])
timeseries.get_intraday(symbol[CONF_SYMBOL])
except ValueError:
_LOGGER.error(
"API Key is not valid or symbol '%s' not known", symbol)
_LOGGER.error("API Key is not valid or symbol '%s' not known", symbol)
dev.append(AlphaVantageSensor(timeseries, symbol))
forex = ForeignExchange(key=api_key)
@ -92,12 +92,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
to_cur = conversion.get(CONF_TO)
try:
_LOGGER.debug("Configuring forex %s - %s", from_cur, to_cur)
forex.get_currency_exchange_rate(
from_currency=from_cur, to_currency=to_cur)
forex.get_currency_exchange_rate(from_currency=from_cur, to_currency=to_cur)
except ValueError as error:
_LOGGER.error(
"API Key is not valid or currencies '%s'/'%s' not known",
from_cur, to_cur)
from_cur,
to_cur,
)
_LOGGER.debug(str(error))
dev.append(AlphaVantageForeignExchange(forex, conversion))
@ -115,7 +116,7 @@ class AlphaVantageSensor(Entity):
self._timeseries = timeseries
self.values = None
self._unit_of_measurement = symbol.get(CONF_CURRENCY, self._symbol)
self._icon = ICONS.get(symbol.get(CONF_CURRENCY, 'USD'))
self._icon = ICONS.get(symbol.get(CONF_CURRENCY, "USD"))
@property
def name(self):
@ -130,7 +131,7 @@ class AlphaVantageSensor(Entity):
@property
def state(self):
"""Return the state of the sensor."""
return self.values['1. open']
return self.values["1. open"]
@property
def device_state_attributes(self):
@ -138,9 +139,9 @@ class AlphaVantageSensor(Entity):
if self.values is not None:
return {
ATTR_ATTRIBUTION: ATTRIBUTION,
ATTR_CLOSE: self.values['4. close'],
ATTR_HIGH: self.values['2. high'],
ATTR_LOW: self.values['3. low'],
ATTR_CLOSE: self.values["4. close"],
ATTR_HIGH: self.values["2. high"],
ATTR_LOW: self.values["3. low"],
}
@property
@ -167,9 +168,9 @@ class AlphaVantageForeignExchange(Entity):
if CONF_NAME in config:
self._name = config.get(CONF_NAME)
else:
self._name = '{}/{}'.format(self._to_currency, self._from_currency)
self._name = "{}/{}".format(self._to_currency, self._from_currency)
self._unit_of_measurement = self._to_currency
self._icon = ICONS.get(self._from_currency, 'USD')
self._icon = ICONS.get(self._from_currency, "USD")
self.values = None
@property
@ -185,7 +186,7 @@ class AlphaVantageForeignExchange(Entity):
@property
def state(self):
"""Return the state of the sensor."""
return round(float(self.values['5. Exchange Rate']), 4)
return round(float(self.values["5. Exchange Rate"]), 4)
@property
def icon(self):
@ -204,9 +205,16 @@ class AlphaVantageForeignExchange(Entity):
def update(self):
"""Get the latest data and updates the states."""
_LOGGER.debug("Requesting new data for forex %s - %s",
self._from_currency, self._to_currency)
_LOGGER.debug(
"Requesting new data for forex %s - %s",
self._from_currency,
self._to_currency,
)
self.values, _ = self._foreign_exchange.get_currency_exchange_rate(
from_currency=self._from_currency, to_currency=self._to_currency)
_LOGGER.debug("Received new data for forex %s - %s",
self._from_currency, self._to_currency)
from_currency=self._from_currency, to_currency=self._to_currency
)
_LOGGER.debug(
"Received new data for forex %s - %s",
self._from_currency,
self._to_currency,
)

View file

@ -8,108 +8,145 @@ import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
CONF_REGION = 'region_name'
CONF_ACCESS_KEY_ID = 'aws_access_key_id'
CONF_SECRET_ACCESS_KEY = 'aws_secret_access_key'
CONF_PROFILE_NAME = 'profile_name'
ATTR_CREDENTIALS = 'credentials'
CONF_REGION = "region_name"
CONF_ACCESS_KEY_ID = "aws_access_key_id"
CONF_SECRET_ACCESS_KEY = "aws_secret_access_key"
CONF_PROFILE_NAME = "profile_name"
ATTR_CREDENTIALS = "credentials"
DEFAULT_REGION = 'us-east-1'
SUPPORTED_REGIONS = ['us-east-1', 'us-east-2', 'us-west-1', 'us-west-2',
'ca-central-1', 'eu-west-1', 'eu-central-1', 'eu-west-2',
'eu-west-3', 'ap-southeast-1', 'ap-southeast-2',
'ap-northeast-2', 'ap-northeast-1', 'ap-south-1',
'sa-east-1']
CONF_VOICE = 'voice'
CONF_OUTPUT_FORMAT = 'output_format'
CONF_SAMPLE_RATE = 'sample_rate'
CONF_TEXT_TYPE = 'text_type'
SUPPORTED_VOICES = [
'Zhiyu', # Chinese
'Mads', 'Naja', # Danish
'Ruben', 'Lotte', # Dutch
'Russell', 'Nicole', # English Austrailian
'Brian', 'Amy', 'Emma', # English
'Aditi', 'Raveena', # English, Indian
'Joey', 'Justin', 'Matthew', 'Ivy', 'Joanna', 'Kendra', 'Kimberly',
'Salli', # English
'Geraint', # English Welsh
'Mathieu', 'Celine', 'Lea', # French
'Chantal', # French Canadian
'Hans', 'Marlene', 'Vicki', # German
'Aditi', # Hindi
'Karl', 'Dora', # Icelandic
'Giorgio', 'Carla', 'Bianca', # Italian
'Takumi', 'Mizuki', # Japanese
'Seoyeon', # Korean
'Liv', # Norwegian
'Jacek', 'Jan', 'Ewa', 'Maja', # Polish
'Ricardo', 'Vitoria', # Portuguese, Brazilian
'Cristiano', 'Ines', # Portuguese, European
'Carmen', # Romanian
'Maxim', 'Tatyana', # Russian
'Enrique', 'Conchita', 'Lucia', # Spanish European
'Mia', # Spanish Mexican
'Miguel', 'Penelope', # Spanish US
'Astrid', # Swedish
'Filiz', # Turkish
'Gwyneth', # Welsh
DEFAULT_REGION = "us-east-1"
SUPPORTED_REGIONS = [
"us-east-1",
"us-east-2",
"us-west-1",
"us-west-2",
"ca-central-1",
"eu-west-1",
"eu-central-1",
"eu-west-2",
"eu-west-3",
"ap-southeast-1",
"ap-southeast-2",
"ap-northeast-2",
"ap-northeast-1",
"ap-south-1",
"sa-east-1",
]
SUPPORTED_OUTPUT_FORMATS = ['mp3', 'ogg_vorbis', 'pcm']
CONF_VOICE = "voice"
CONF_OUTPUT_FORMAT = "output_format"
CONF_SAMPLE_RATE = "sample_rate"
CONF_TEXT_TYPE = "text_type"
SUPPORTED_SAMPLE_RATES = ['8000', '16000', '22050']
SUPPORTED_VOICES = [
"Zhiyu", # Chinese
"Mads",
"Naja", # Danish
"Ruben",
"Lotte", # Dutch
"Russell",
"Nicole", # English Austrailian
"Brian",
"Amy",
"Emma", # English
"Aditi",
"Raveena", # English, Indian
"Joey",
"Justin",
"Matthew",
"Ivy",
"Joanna",
"Kendra",
"Kimberly",
"Salli", # English
"Geraint", # English Welsh
"Mathieu",
"Celine",
"Lea", # French
"Chantal", # French Canadian
"Hans",
"Marlene",
"Vicki", # German
"Aditi", # Hindi
"Karl",
"Dora", # Icelandic
"Giorgio",
"Carla",
"Bianca", # Italian
"Takumi",
"Mizuki", # Japanese
"Seoyeon", # Korean
"Liv", # Norwegian
"Jacek",
"Jan",
"Ewa",
"Maja", # Polish
"Ricardo",
"Vitoria", # Portuguese, Brazilian
"Cristiano",
"Ines", # Portuguese, European
"Carmen", # Romanian
"Maxim",
"Tatyana", # Russian
"Enrique",
"Conchita",
"Lucia", # Spanish European
"Mia", # Spanish Mexican
"Miguel",
"Penelope", # Spanish US
"Astrid", # Swedish
"Filiz", # Turkish
"Gwyneth", # Welsh
]
SUPPORTED_OUTPUT_FORMATS = ["mp3", "ogg_vorbis", "pcm"]
SUPPORTED_SAMPLE_RATES = ["8000", "16000", "22050"]
SUPPORTED_SAMPLE_RATES_MAP = {
'mp3': ['8000', '16000', '22050'],
'ogg_vorbis': ['8000', '16000', '22050'],
'pcm': ['8000', '16000'],
"mp3": ["8000", "16000", "22050"],
"ogg_vorbis": ["8000", "16000", "22050"],
"pcm": ["8000", "16000"],
}
SUPPORTED_TEXT_TYPES = ['text', 'ssml']
SUPPORTED_TEXT_TYPES = ["text", "ssml"]
CONTENT_TYPE_EXTENSIONS = {
'audio/mpeg': 'mp3',
'audio/ogg': 'ogg',
'audio/pcm': 'pcm',
}
CONTENT_TYPE_EXTENSIONS = {"audio/mpeg": "mp3", "audio/ogg": "ogg", "audio/pcm": "pcm"}
DEFAULT_VOICE = 'Joanna'
DEFAULT_OUTPUT_FORMAT = 'mp3'
DEFAULT_TEXT_TYPE = 'text'
DEFAULT_VOICE = "Joanna"
DEFAULT_OUTPUT_FORMAT = "mp3"
DEFAULT_TEXT_TYPE = "text"
DEFAULT_SAMPLE_RATES = {
'mp3': '22050',
'ogg_vorbis': '22050',
'pcm': '16000',
}
DEFAULT_SAMPLE_RATES = {"mp3": "22050", "ogg_vorbis": "22050", "pcm": "16000"}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_REGION, default=DEFAULT_REGION):
vol.In(SUPPORTED_REGIONS),
vol.Inclusive(CONF_ACCESS_KEY_ID, ATTR_CREDENTIALS): cv.string,
vol.Inclusive(CONF_SECRET_ACCESS_KEY, ATTR_CREDENTIALS): cv.string,
vol.Exclusive(CONF_PROFILE_NAME, ATTR_CREDENTIALS): cv.string,
vol.Optional(CONF_VOICE, default=DEFAULT_VOICE): vol.In(SUPPORTED_VOICES),
vol.Optional(CONF_OUTPUT_FORMAT, default=DEFAULT_OUTPUT_FORMAT):
vol.In(SUPPORTED_OUTPUT_FORMATS),
vol.Optional(CONF_SAMPLE_RATE):
vol.All(cv.string, vol.In(SUPPORTED_SAMPLE_RATES)),
vol.Optional(CONF_TEXT_TYPE, default=DEFAULT_TEXT_TYPE):
vol.In(SUPPORTED_TEXT_TYPES),
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Optional(CONF_REGION, default=DEFAULT_REGION): vol.In(SUPPORTED_REGIONS),
vol.Inclusive(CONF_ACCESS_KEY_ID, ATTR_CREDENTIALS): cv.string,
vol.Inclusive(CONF_SECRET_ACCESS_KEY, ATTR_CREDENTIALS): cv.string,
vol.Exclusive(CONF_PROFILE_NAME, ATTR_CREDENTIALS): cv.string,
vol.Optional(CONF_VOICE, default=DEFAULT_VOICE): vol.In(SUPPORTED_VOICES),
vol.Optional(CONF_OUTPUT_FORMAT, default=DEFAULT_OUTPUT_FORMAT): vol.In(
SUPPORTED_OUTPUT_FORMATS
),
vol.Optional(CONF_SAMPLE_RATE): vol.All(
cv.string, vol.In(SUPPORTED_SAMPLE_RATES)
),
vol.Optional(CONF_TEXT_TYPE, default=DEFAULT_TEXT_TYPE): vol.In(
SUPPORTED_TEXT_TYPES
),
}
)
def get_engine(hass, config):
"""Set up Amazon Polly speech component."""
output_format = config.get(CONF_OUTPUT_FORMAT)
sample_rate = config.get(
CONF_SAMPLE_RATE, DEFAULT_SAMPLE_RATES[output_format])
sample_rate = config.get(CONF_SAMPLE_RATE, DEFAULT_SAMPLE_RATES[output_format])
if sample_rate not in SUPPORTED_SAMPLE_RATES_MAP.get(output_format):
_LOGGER.error("%s is not a valid sample rate for %s",
sample_rate, output_format)
_LOGGER.error(
"%s is not a valid sample rate for %s", sample_rate, output_format
)
return None
config[CONF_SAMPLE_RATE] = sample_rate
@ -131,7 +168,7 @@ def get_engine(hass, config):
del config[CONF_ACCESS_KEY_ID]
del config[CONF_SECRET_ACCESS_KEY]
polly_client = boto3.client('polly', **aws_config)
polly_client = boto3.client("polly", **aws_config)
supported_languages = []
@ -139,27 +176,25 @@ def get_engine(hass, config):
all_voices_req = polly_client.describe_voices()
for voice in all_voices_req.get('Voices'):
all_voices[voice.get('Id')] = voice
if voice.get('LanguageCode') not in supported_languages:
supported_languages.append(voice.get('LanguageCode'))
for voice in all_voices_req.get("Voices"):
all_voices[voice.get("Id")] = voice
if voice.get("LanguageCode") not in supported_languages:
supported_languages.append(voice.get("LanguageCode"))
return AmazonPollyProvider(
polly_client, config, supported_languages, all_voices)
return AmazonPollyProvider(polly_client, config, supported_languages, all_voices)
class AmazonPollyProvider(Provider):
"""Amazon Polly speech api provider."""
def __init__(self, polly_client, config, supported_languages,
all_voices):
def __init__(self, polly_client, config, supported_languages, all_voices):
"""Initialize Amazon Polly provider for TTS."""
self.client = polly_client
self.config = config
self.supported_langs = supported_languages
self.all_voices = all_voices
self.default_voice = self.config.get(CONF_VOICE)
self.name = 'Amazon Polly'
self.name = "Amazon Polly"
@property
def supported_languages(self):
@ -169,7 +204,7 @@ class AmazonPollyProvider(Provider):
@property
def default_language(self):
"""Return the default language."""
return self.all_voices.get(self.default_voice).get('LanguageCode')
return self.all_voices.get(self.default_voice).get("LanguageCode")
@property
def default_options(self):
@ -185,9 +220,8 @@ class AmazonPollyProvider(Provider):
"""Request TTS file from Polly."""
voice_id = options.get(CONF_VOICE, self.default_voice)
voice_in_dict = self.all_voices.get(voice_id)
if language != voice_in_dict.get('LanguageCode'):
_LOGGER.error("%s does not support the %s language",
voice_id, language)
if language != voice_in_dict.get("LanguageCode"):
_LOGGER.error("%s does not support the %s language", voice_id, language)
return None, None
resp = self.client.synthesize_speech(
@ -195,8 +229,10 @@ class AmazonPollyProvider(Provider):
SampleRate=self.config[CONF_SAMPLE_RATE],
Text=message,
TextType=self.config[CONF_TEXT_TYPE],
VoiceId=voice_id
VoiceId=voice_id,
)
return (CONTENT_TYPE_EXTENSIONS[resp.get('ContentType')],
resp.get('AudioStream').read())
return (
CONTENT_TYPE_EXTENSIONS[resp.get("ContentType")],
resp.get("AudioStream").read(),
)

View file

@ -12,11 +12,12 @@ _LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN:
vol.Schema({
DOMAIN: vol.Schema(
{
vol.Required(CONF_CLIENT_ID): cv.string,
vol.Required(CONF_CLIENT_SECRET): cv.string,
})
}
)
},
extra=vol.ALLOW_EXTRA,
)
@ -30,15 +31,16 @@ async def async_setup(hass, config):
conf = config[DOMAIN]
config_flow.register_flow_implementation(
hass, conf[CONF_CLIENT_ID],
conf[CONF_CLIENT_SECRET])
hass, conf[CONF_CLIENT_ID], conf[CONF_CLIENT_SECRET]
)
return True
async def async_setup_entry(hass, entry):
"""Set up Ambiclimate from a config entry."""
hass.async_create_task(hass.config_entries.async_forward_entry_setup(
entry, 'climate'))
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, "climate")
)
return True

View file

@ -7,35 +7,41 @@ import voluptuous as vol
from homeassistant.components.climate import ClimateDevice
from homeassistant.components.climate.const import (
SUPPORT_TARGET_TEMPERATURE, HVAC_MODE_OFF, HVAC_MODE_HEAT)
SUPPORT_TARGET_TEMPERATURE,
HVAC_MODE_OFF,
HVAC_MODE_HEAT,
)
from homeassistant.const import ATTR_NAME, ATTR_TEMPERATURE, TEMP_CELSIUS
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import (ATTR_VALUE, CONF_CLIENT_ID, CONF_CLIENT_SECRET,
DOMAIN, SERVICE_COMFORT_FEEDBACK, SERVICE_COMFORT_MODE,
SERVICE_TEMPERATURE_MODE, STORAGE_KEY, STORAGE_VERSION)
from .const import (
ATTR_VALUE,
CONF_CLIENT_ID,
CONF_CLIENT_SECRET,
DOMAIN,
SERVICE_COMFORT_FEEDBACK,
SERVICE_COMFORT_MODE,
SERVICE_TEMPERATURE_MODE,
STORAGE_KEY,
STORAGE_VERSION,
)
_LOGGER = logging.getLogger(__name__)
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE
SEND_COMFORT_FEEDBACK_SCHEMA = vol.Schema({
vol.Required(ATTR_NAME): cv.string,
vol.Required(ATTR_VALUE): cv.string,
})
SEND_COMFORT_FEEDBACK_SCHEMA = vol.Schema(
{vol.Required(ATTR_NAME): cv.string, vol.Required(ATTR_VALUE): cv.string}
)
SET_COMFORT_MODE_SCHEMA = vol.Schema({
vol.Required(ATTR_NAME): cv.string,
})
SET_COMFORT_MODE_SCHEMA = vol.Schema({vol.Required(ATTR_NAME): cv.string})
SET_TEMPERATURE_MODE_SCHEMA = vol.Schema({
vol.Required(ATTR_NAME): cv.string,
vol.Required(ATTR_VALUE): cv.string,
})
SET_TEMPERATURE_MODE_SCHEMA = vol.Schema(
{vol.Required(ATTR_NAME): cv.string, vol.Required(ATTR_VALUE): cv.string}
)
async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the Ambicliamte device."""
@ -46,10 +52,12 @@ async def async_setup_entry(hass, entry, async_add_entities):
store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
token_info = await store.async_load()
oauth = ambiclimate.AmbiclimateOAuth(config[CONF_CLIENT_ID],
config[CONF_CLIENT_SECRET],
config['callback_url'],
websession)
oauth = ambiclimate.AmbiclimateOAuth(
config[CONF_CLIENT_ID],
config[CONF_CLIENT_SECRET],
config["callback_url"],
websession,
)
try:
token_info = await oauth.refresh_access_token(token_info)
@ -62,9 +70,9 @@ async def async_setup_entry(hass, entry, async_add_entities):
await store.async_save(token_info)
data_connection = ambiclimate.AmbiclimateConnection(oauth,
token_info=token_info,
websession=websession)
data_connection = ambiclimate.AmbiclimateConnection(
oauth, token_info=token_info, websession=websession
)
if not await data_connection.find_devices():
_LOGGER.error("No devices found")
@ -88,10 +96,12 @@ async def async_setup_entry(hass, entry, async_add_entities):
if device:
await device.set_comfort_feedback(service.data[ATTR_VALUE])
hass.services.async_register(DOMAIN,
SERVICE_COMFORT_FEEDBACK,
send_comfort_feedback,
schema=SEND_COMFORT_FEEDBACK_SCHEMA)
hass.services.async_register(
DOMAIN,
SERVICE_COMFORT_FEEDBACK,
send_comfort_feedback,
schema=SEND_COMFORT_FEEDBACK_SCHEMA,
)
async def set_comfort_mode(service):
"""Set comfort mode."""
@ -100,10 +110,9 @@ async def async_setup_entry(hass, entry, async_add_entities):
if device:
await device.set_comfort_mode()
hass.services.async_register(DOMAIN,
SERVICE_COMFORT_MODE,
set_comfort_mode,
schema=SET_COMFORT_MODE_SCHEMA)
hass.services.async_register(
DOMAIN, SERVICE_COMFORT_MODE, set_comfort_mode, schema=SET_COMFORT_MODE_SCHEMA
)
async def set_temperature_mode(service):
"""Set temperature mode."""
@ -112,10 +121,12 @@ async def async_setup_entry(hass, entry, async_add_entities):
if device:
await device.set_temperature_mode(service.data[ATTR_VALUE])
hass.services.async_register(DOMAIN,
SERVICE_TEMPERATURE_MODE,
set_temperature_mode,
schema=SET_TEMPERATURE_MODE_SCHEMA)
hass.services.async_register(
DOMAIN,
SERVICE_TEMPERATURE_MODE,
set_temperature_mode,
schema=SET_TEMPERATURE_MODE_SCHEMA,
)
class AmbiclimateEntity(ClimateDevice):
@ -141,11 +152,9 @@ class AmbiclimateEntity(ClimateDevice):
def device_info(self):
"""Return the device info."""
return {
'identifiers': {
(DOMAIN, self.unique_id)
},
'name': self.name,
'manufacturer': 'Ambiclimate',
"identifiers": {(DOMAIN, self.unique_id)},
"name": self.name,
"manufacturer": "Ambiclimate",
}
@property
@ -156,7 +165,7 @@ class AmbiclimateEntity(ClimateDevice):
@property
def target_temperature(self):
"""Return the target temperature."""
return self._data.get('target_temperature')
return self._data.get("target_temperature")
@property
def target_temperature_step(self):
@ -166,12 +175,12 @@ class AmbiclimateEntity(ClimateDevice):
@property
def current_temperature(self):
"""Return the current temperature."""
return self._data.get('temperature')
return self._data.get("temperature")
@property
def current_humidity(self):
"""Return the current humidity."""
return self._data.get('humidity')
return self._data.get("humidity")
@property
def min_temp(self):
@ -196,7 +205,7 @@ class AmbiclimateEntity(ClimateDevice):
@property
def hvac_mode(self):
"""Return current operation."""
if self._data.get('power', '').lower() == 'on':
if self._data.get("power", "").lower() == "on":
return HVAC_MODE_HEAT
return HVAC_MODE_OFF

View file

@ -7,10 +7,17 @@ from homeassistant import config_entries
from homeassistant.components.http import HomeAssistantView
from homeassistant.core import callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import (AUTH_CALLBACK_NAME, AUTH_CALLBACK_PATH, CONF_CLIENT_ID,
CONF_CLIENT_SECRET, DOMAIN, STORAGE_VERSION, STORAGE_KEY)
from .const import (
AUTH_CALLBACK_NAME,
AUTH_CALLBACK_PATH,
CONF_CLIENT_ID,
CONF_CLIENT_SECRET,
DOMAIN,
STORAGE_VERSION,
STORAGE_KEY,
)
DATA_AMBICLIMATE_IMPL = 'ambiclimate_flow_implementation'
DATA_AMBICLIMATE_IMPL = "ambiclimate_flow_implementation"
_LOGGER = logging.getLogger(__name__)
@ -30,7 +37,7 @@ def register_flow_implementation(hass, client_id, client_secret):
}
@config_entries.HANDLERS.register('ambiclimate')
@config_entries.HANDLERS.register("ambiclimate")
class AmbiclimateFlowHandler(config_entries.ConfigFlow):
"""Handle a config flow."""
@ -45,54 +52,52 @@ class AmbiclimateFlowHandler(config_entries.ConfigFlow):
async def async_step_user(self, user_input=None):
"""Handle external yaml configuration."""
if self.hass.config_entries.async_entries(DOMAIN):
return self.async_abort(reason='already_setup')
return self.async_abort(reason="already_setup")
config = self.hass.data.get(DATA_AMBICLIMATE_IMPL, {})
if not config:
_LOGGER.debug("No config")
return self.async_abort(reason='no_config')
return self.async_abort(reason="no_config")
return await self.async_step_auth()
async def async_step_auth(self, user_input=None):
"""Handle a flow start."""
if self.hass.config_entries.async_entries(DOMAIN):
return self.async_abort(reason='already_setup')
return self.async_abort(reason="already_setup")
errors = {}
if user_input is not None:
errors['base'] = 'follow_link'
errors["base"] = "follow_link"
if not self._registered_view:
self._generate_view()
return self.async_show_form(
step_id='auth',
description_placeholders={'authorization_url':
await self._get_authorize_url(),
'cb_url': self._cb_url()},
step_id="auth",
description_placeholders={
"authorization_url": await self._get_authorize_url(),
"cb_url": self._cb_url(),
},
errors=errors,
)
async def async_step_code(self, code=None):
"""Received code for authentication."""
if self.hass.config_entries.async_entries(DOMAIN):
return self.async_abort(reason='already_setup')
return self.async_abort(reason="already_setup")
token_info = await self._get_token_info(code)
if token_info is None:
return self.async_abort(reason='access_token')
return self.async_abort(reason="access_token")
config = self.hass.data[DATA_AMBICLIMATE_IMPL].copy()
config['callback_url'] = self._cb_url()
config["callback_url"] = self._cb_url()
return self.async_create_entry(
title="Ambiclimate",
data=config,
)
return self.async_create_entry(title="Ambiclimate", data=config)
async def _get_token_info(self, code):
oauth = self._generate_oauth()
@ -116,15 +121,16 @@ class AmbiclimateFlowHandler(config_entries.ConfigFlow):
clientsession = async_get_clientsession(self.hass)
callback_url = self._cb_url()
oauth = ambiclimate.AmbiclimateOAuth(config.get(CONF_CLIENT_ID),
config.get(CONF_CLIENT_SECRET),
callback_url,
clientsession)
oauth = ambiclimate.AmbiclimateOAuth(
config.get(CONF_CLIENT_ID),
config.get(CONF_CLIENT_SECRET),
callback_url,
clientsession,
)
return oauth
def _cb_url(self):
return '{}{}'.format(self.hass.config.api.base_url,
AUTH_CALLBACK_PATH)
return "{}{}".format(self.hass.config.api.base_url, AUTH_CALLBACK_PATH)
async def _get_authorize_url(self):
oauth = self._generate_oauth()
@ -140,14 +146,13 @@ class AmbiclimateAuthCallbackView(HomeAssistantView):
async def get(self, request):
"""Receive authorization token."""
code = request.query.get('code')
code = request.query.get("code")
if code is None:
return "No code"
hass = request.app['hass']
hass = request.app["hass"]
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
context={'source': 'code'},
data=code,
))
DOMAIN, context={"source": "code"}, data=code
)
)
return "OK!"

View file

@ -1,14 +1,14 @@
"""Constants used by the Ambiclimate component."""
ATTR_VALUE = 'value'
CONF_CLIENT_ID = 'client_id'
CONF_CLIENT_SECRET = 'client_secret'
DOMAIN = 'ambiclimate'
SERVICE_COMFORT_FEEDBACK = 'send_comfort_feedback'
SERVICE_COMFORT_MODE = 'set_comfort_mode'
SERVICE_TEMPERATURE_MODE = 'set_temperature_mode'
STORAGE_KEY = 'ambiclimate_auth'
ATTR_VALUE = "value"
CONF_CLIENT_ID = "client_id"
CONF_CLIENT_SECRET = "client_secret"
DOMAIN = "ambiclimate"
SERVICE_COMFORT_FEEDBACK = "send_comfort_feedback"
SERVICE_COMFORT_MODE = "set_comfort_mode"
SERVICE_TEMPERATURE_MODE = "set_temperature_mode"
STORAGE_KEY = "ambiclimate_auth"
STORAGE_VERSION = 1
AUTH_CALLBACK_NAME = 'api:ambiclimate'
AUTH_CALLBACK_PATH = '/api/ambiclimate'
AUTH_CALLBACK_NAME = "api:ambiclimate"
AUTH_CALLBACK_PATH = "/api/ambiclimate"

View file

@ -7,220 +7,235 @@ import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import (
ATTR_NAME, ATTR_LOCATION, CONF_API_KEY, EVENT_HOMEASSISTANT_STOP)
ATTR_NAME,
ATTR_LOCATION,
CONF_API_KEY,
EVENT_HOMEASSISTANT_STOP,
)
from homeassistant.core import callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client, config_validation as cv
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect, async_dispatcher_send)
async_dispatcher_connect,
async_dispatcher_send,
)
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import async_call_later
from .config_flow import configured_instances
from .const import (
ATTR_LAST_DATA, CONF_APP_KEY, DATA_CLIENT, DOMAIN, TOPIC_UPDATE,
TYPE_BINARY_SENSOR, TYPE_SENSOR)
ATTR_LAST_DATA,
CONF_APP_KEY,
DATA_CLIENT,
DOMAIN,
TOPIC_UPDATE,
TYPE_BINARY_SENSOR,
TYPE_SENSOR,
)
_LOGGER = logging.getLogger(__name__)
DATA_CONFIG = 'config'
DATA_CONFIG = "config"
DEFAULT_SOCKET_MIN_RETRY = 15
DEFAULT_WATCHDOG_SECONDS = 5 * 60
TYPE_24HOURRAININ = '24hourrainin'
TYPE_BAROMABSIN = 'baromabsin'
TYPE_BAROMRELIN = 'baromrelin'
TYPE_BATT1 = 'batt1'
TYPE_BATT10 = 'batt10'
TYPE_BATT2 = 'batt2'
TYPE_BATT3 = 'batt3'
TYPE_BATT4 = 'batt4'
TYPE_BATT5 = 'batt5'
TYPE_BATT6 = 'batt6'
TYPE_BATT7 = 'batt7'
TYPE_BATT8 = 'batt8'
TYPE_BATT9 = 'batt9'
TYPE_BATTOUT = 'battout'
TYPE_CO2 = 'co2'
TYPE_DAILYRAININ = 'dailyrainin'
TYPE_DEWPOINT = 'dewPoint'
TYPE_EVENTRAININ = 'eventrainin'
TYPE_FEELSLIKE = 'feelsLike'
TYPE_HOURLYRAININ = 'hourlyrainin'
TYPE_HUMIDITY = 'humidity'
TYPE_HUMIDITY1 = 'humidity1'
TYPE_HUMIDITY10 = 'humidity10'
TYPE_HUMIDITY2 = 'humidity2'
TYPE_HUMIDITY3 = 'humidity3'
TYPE_HUMIDITY4 = 'humidity4'
TYPE_HUMIDITY5 = 'humidity5'
TYPE_HUMIDITY6 = 'humidity6'
TYPE_HUMIDITY7 = 'humidity7'
TYPE_HUMIDITY8 = 'humidity8'
TYPE_HUMIDITY9 = 'humidity9'
TYPE_HUMIDITYIN = 'humidityin'
TYPE_LASTRAIN = 'lastRain'
TYPE_MAXDAILYGUST = 'maxdailygust'
TYPE_MONTHLYRAININ = 'monthlyrainin'
TYPE_RELAY1 = 'relay1'
TYPE_RELAY10 = 'relay10'
TYPE_RELAY2 = 'relay2'
TYPE_RELAY3 = 'relay3'
TYPE_RELAY4 = 'relay4'
TYPE_RELAY5 = 'relay5'
TYPE_RELAY6 = 'relay6'
TYPE_RELAY7 = 'relay7'
TYPE_RELAY8 = 'relay8'
TYPE_RELAY9 = 'relay9'
TYPE_SOILHUM1 = 'soilhum1'
TYPE_SOILHUM10 = 'soilhum10'
TYPE_SOILHUM2 = 'soilhum2'
TYPE_SOILHUM3 = 'soilhum3'
TYPE_SOILHUM4 = 'soilhum4'
TYPE_SOILHUM5 = 'soilhum5'
TYPE_SOILHUM6 = 'soilhum6'
TYPE_SOILHUM7 = 'soilhum7'
TYPE_SOILHUM8 = 'soilhum8'
TYPE_SOILHUM9 = 'soilhum9'
TYPE_SOILTEMP1F = 'soiltemp1f'
TYPE_SOILTEMP10F = 'soiltemp10f'
TYPE_SOILTEMP2F = 'soiltemp2f'
TYPE_SOILTEMP3F = 'soiltemp3f'
TYPE_SOILTEMP4F = 'soiltemp4f'
TYPE_SOILTEMP5F = 'soiltemp5f'
TYPE_SOILTEMP6F = 'soiltemp6f'
TYPE_SOILTEMP7F = 'soiltemp7f'
TYPE_SOILTEMP8F = 'soiltemp8f'
TYPE_SOILTEMP9F = 'soiltemp9f'
TYPE_SOLARRADIATION = 'solarradiation'
TYPE_SOLARRADIATION_LX = 'solarradiation_lx'
TYPE_TEMP10F = 'temp10f'
TYPE_TEMP1F = 'temp1f'
TYPE_TEMP2F = 'temp2f'
TYPE_TEMP3F = 'temp3f'
TYPE_TEMP4F = 'temp4f'
TYPE_TEMP5F = 'temp5f'
TYPE_TEMP6F = 'temp6f'
TYPE_TEMP7F = 'temp7f'
TYPE_TEMP8F = 'temp8f'
TYPE_TEMP9F = 'temp9f'
TYPE_TEMPF = 'tempf'
TYPE_TEMPINF = 'tempinf'
TYPE_TOTALRAININ = 'totalrainin'
TYPE_UV = 'uv'
TYPE_WEEKLYRAININ = 'weeklyrainin'
TYPE_WINDDIR = 'winddir'
TYPE_WINDDIR_AVG10M = 'winddir_avg10m'
TYPE_WINDDIR_AVG2M = 'winddir_avg2m'
TYPE_WINDGUSTDIR = 'windgustdir'
TYPE_WINDGUSTMPH = 'windgustmph'
TYPE_WINDSPDMPH_AVG10M = 'windspdmph_avg10m'
TYPE_WINDSPDMPH_AVG2M = 'windspdmph_avg2m'
TYPE_WINDSPEEDMPH = 'windspeedmph'
TYPE_YEARLYRAININ = 'yearlyrainin'
TYPE_24HOURRAININ = "24hourrainin"
TYPE_BAROMABSIN = "baromabsin"
TYPE_BAROMRELIN = "baromrelin"
TYPE_BATT1 = "batt1"
TYPE_BATT10 = "batt10"
TYPE_BATT2 = "batt2"
TYPE_BATT3 = "batt3"
TYPE_BATT4 = "batt4"
TYPE_BATT5 = "batt5"
TYPE_BATT6 = "batt6"
TYPE_BATT7 = "batt7"
TYPE_BATT8 = "batt8"
TYPE_BATT9 = "batt9"
TYPE_BATTOUT = "battout"
TYPE_CO2 = "co2"
TYPE_DAILYRAININ = "dailyrainin"
TYPE_DEWPOINT = "dewPoint"
TYPE_EVENTRAININ = "eventrainin"
TYPE_FEELSLIKE = "feelsLike"
TYPE_HOURLYRAININ = "hourlyrainin"
TYPE_HUMIDITY = "humidity"
TYPE_HUMIDITY1 = "humidity1"
TYPE_HUMIDITY10 = "humidity10"
TYPE_HUMIDITY2 = "humidity2"
TYPE_HUMIDITY3 = "humidity3"
TYPE_HUMIDITY4 = "humidity4"
TYPE_HUMIDITY5 = "humidity5"
TYPE_HUMIDITY6 = "humidity6"
TYPE_HUMIDITY7 = "humidity7"
TYPE_HUMIDITY8 = "humidity8"
TYPE_HUMIDITY9 = "humidity9"
TYPE_HUMIDITYIN = "humidityin"
TYPE_LASTRAIN = "lastRain"
TYPE_MAXDAILYGUST = "maxdailygust"
TYPE_MONTHLYRAININ = "monthlyrainin"
TYPE_RELAY1 = "relay1"
TYPE_RELAY10 = "relay10"
TYPE_RELAY2 = "relay2"
TYPE_RELAY3 = "relay3"
TYPE_RELAY4 = "relay4"
TYPE_RELAY5 = "relay5"
TYPE_RELAY6 = "relay6"
TYPE_RELAY7 = "relay7"
TYPE_RELAY8 = "relay8"
TYPE_RELAY9 = "relay9"
TYPE_SOILHUM1 = "soilhum1"
TYPE_SOILHUM10 = "soilhum10"
TYPE_SOILHUM2 = "soilhum2"
TYPE_SOILHUM3 = "soilhum3"
TYPE_SOILHUM4 = "soilhum4"
TYPE_SOILHUM5 = "soilhum5"
TYPE_SOILHUM6 = "soilhum6"
TYPE_SOILHUM7 = "soilhum7"
TYPE_SOILHUM8 = "soilhum8"
TYPE_SOILHUM9 = "soilhum9"
TYPE_SOILTEMP1F = "soiltemp1f"
TYPE_SOILTEMP10F = "soiltemp10f"
TYPE_SOILTEMP2F = "soiltemp2f"
TYPE_SOILTEMP3F = "soiltemp3f"
TYPE_SOILTEMP4F = "soiltemp4f"
TYPE_SOILTEMP5F = "soiltemp5f"
TYPE_SOILTEMP6F = "soiltemp6f"
TYPE_SOILTEMP7F = "soiltemp7f"
TYPE_SOILTEMP8F = "soiltemp8f"
TYPE_SOILTEMP9F = "soiltemp9f"
TYPE_SOLARRADIATION = "solarradiation"
TYPE_SOLARRADIATION_LX = "solarradiation_lx"
TYPE_TEMP10F = "temp10f"
TYPE_TEMP1F = "temp1f"
TYPE_TEMP2F = "temp2f"
TYPE_TEMP3F = "temp3f"
TYPE_TEMP4F = "temp4f"
TYPE_TEMP5F = "temp5f"
TYPE_TEMP6F = "temp6f"
TYPE_TEMP7F = "temp7f"
TYPE_TEMP8F = "temp8f"
TYPE_TEMP9F = "temp9f"
TYPE_TEMPF = "tempf"
TYPE_TEMPINF = "tempinf"
TYPE_TOTALRAININ = "totalrainin"
TYPE_UV = "uv"
TYPE_WEEKLYRAININ = "weeklyrainin"
TYPE_WINDDIR = "winddir"
TYPE_WINDDIR_AVG10M = "winddir_avg10m"
TYPE_WINDDIR_AVG2M = "winddir_avg2m"
TYPE_WINDGUSTDIR = "windgustdir"
TYPE_WINDGUSTMPH = "windgustmph"
TYPE_WINDSPDMPH_AVG10M = "windspdmph_avg10m"
TYPE_WINDSPDMPH_AVG2M = "windspdmph_avg2m"
TYPE_WINDSPEEDMPH = "windspeedmph"
TYPE_YEARLYRAININ = "yearlyrainin"
SENSOR_TYPES = {
TYPE_24HOURRAININ: ('24 Hr Rain', 'in', TYPE_SENSOR, None),
TYPE_BAROMABSIN: ('Abs Pressure', 'inHg', TYPE_SENSOR, 'pressure'),
TYPE_BAROMRELIN: ('Rel Pressure', 'inHg', TYPE_SENSOR, 'pressure'),
TYPE_BATT10: ('Battery 10', None, TYPE_BINARY_SENSOR, 'battery'),
TYPE_BATT1: ('Battery 1', None, TYPE_BINARY_SENSOR, 'battery'),
TYPE_BATT2: ('Battery 2', None, TYPE_BINARY_SENSOR, 'battery'),
TYPE_BATT3: ('Battery 3', None, TYPE_BINARY_SENSOR, 'battery'),
TYPE_BATT4: ('Battery 4', None, TYPE_BINARY_SENSOR, 'battery'),
TYPE_BATT5: ('Battery 5', None, TYPE_BINARY_SENSOR, 'battery'),
TYPE_BATT6: ('Battery 6', None, TYPE_BINARY_SENSOR, 'battery'),
TYPE_BATT7: ('Battery 7', None, TYPE_BINARY_SENSOR, 'battery'),
TYPE_BATT8: ('Battery 8', None, TYPE_BINARY_SENSOR, 'battery'),
TYPE_BATT9: ('Battery 9', None, TYPE_BINARY_SENSOR, 'battery'),
TYPE_BATTOUT: ('Battery', None, TYPE_BINARY_SENSOR, 'battery'),
TYPE_CO2: ('co2', 'ppm', TYPE_SENSOR, None),
TYPE_DAILYRAININ: ('Daily Rain', 'in', TYPE_SENSOR, None),
TYPE_DEWPOINT: ('Dew Point', '°F', TYPE_SENSOR, 'temperature'),
TYPE_EVENTRAININ: ('Event Rain', 'in', TYPE_SENSOR, None),
TYPE_FEELSLIKE: ('Feels Like', '°F', TYPE_SENSOR, 'temperature'),
TYPE_HOURLYRAININ: ('Hourly Rain Rate', 'in/hr', TYPE_SENSOR, None),
TYPE_HUMIDITY10: ('Humidity 10', '%', TYPE_SENSOR, 'humidity'),
TYPE_HUMIDITY1: ('Humidity 1', '%', TYPE_SENSOR, 'humidity'),
TYPE_HUMIDITY2: ('Humidity 2', '%', TYPE_SENSOR, 'humidity'),
TYPE_HUMIDITY3: ('Humidity 3', '%', TYPE_SENSOR, 'humidity'),
TYPE_HUMIDITY4: ('Humidity 4', '%', TYPE_SENSOR, 'humidity'),
TYPE_HUMIDITY5: ('Humidity 5', '%', TYPE_SENSOR, 'humidity'),
TYPE_HUMIDITY6: ('Humidity 6', '%', TYPE_SENSOR, 'humidity'),
TYPE_HUMIDITY7: ('Humidity 7', '%', TYPE_SENSOR, 'humidity'),
TYPE_HUMIDITY8: ('Humidity 8', '%', TYPE_SENSOR, 'humidity'),
TYPE_HUMIDITY9: ('Humidity 9', '%', TYPE_SENSOR, 'humidity'),
TYPE_HUMIDITY: ('Humidity', '%', TYPE_SENSOR, 'humidity'),
TYPE_HUMIDITYIN: ('Humidity In', '%', TYPE_SENSOR, 'humidity'),
TYPE_LASTRAIN: ('Last Rain', None, TYPE_SENSOR, 'timestamp'),
TYPE_MAXDAILYGUST: ('Max Gust', 'mph', TYPE_SENSOR, None),
TYPE_MONTHLYRAININ: ('Monthly Rain', 'in', TYPE_SENSOR, None),
TYPE_RELAY10: ('Relay 10', None, TYPE_BINARY_SENSOR, 'connectivity'),
TYPE_RELAY1: ('Relay 1', None, TYPE_BINARY_SENSOR, 'connectivity'),
TYPE_RELAY2: ('Relay 2', None, TYPE_BINARY_SENSOR, 'connectivity'),
TYPE_RELAY3: ('Relay 3', None, TYPE_BINARY_SENSOR, 'connectivity'),
TYPE_RELAY4: ('Relay 4', None, TYPE_BINARY_SENSOR, 'connectivity'),
TYPE_RELAY5: ('Relay 5', None, TYPE_BINARY_SENSOR, 'connectivity'),
TYPE_RELAY6: ('Relay 6', None, TYPE_BINARY_SENSOR, 'connectivity'),
TYPE_RELAY7: ('Relay 7', None, TYPE_BINARY_SENSOR, 'connectivity'),
TYPE_RELAY8: ('Relay 8', None, TYPE_BINARY_SENSOR, 'connectivity'),
TYPE_RELAY9: ('Relay 9', None, TYPE_BINARY_SENSOR, 'connectivity'),
TYPE_SOILHUM10: ('Soil Humidity 10', '%', TYPE_SENSOR, 'humidity'),
TYPE_SOILHUM1: ('Soil Humidity 1', '%', TYPE_SENSOR, 'humidity'),
TYPE_SOILHUM2: ('Soil Humidity 2', '%', TYPE_SENSOR, 'humidity'),
TYPE_SOILHUM3: ('Soil Humidity 3', '%', TYPE_SENSOR, 'humidity'),
TYPE_SOILHUM4: ('Soil Humidity 4', '%', TYPE_SENSOR, 'humidity'),
TYPE_SOILHUM5: ('Soil Humidity 5', '%', TYPE_SENSOR, 'humidity'),
TYPE_SOILHUM6: ('Soil Humidity 6', '%', TYPE_SENSOR, 'humidity'),
TYPE_SOILHUM7: ('Soil Humidity 7', '%', TYPE_SENSOR, 'humidity'),
TYPE_SOILHUM8: ('Soil Humidity 8', '%', TYPE_SENSOR, 'humidity'),
TYPE_SOILHUM9: ('Soil Humidity 9', '%', TYPE_SENSOR, 'humidity'),
TYPE_SOILTEMP10F: ('Soil Temp 10', '°F', TYPE_SENSOR, 'temperature'),
TYPE_SOILTEMP1F: ('Soil Temp 1', '°F', TYPE_SENSOR, 'temperature'),
TYPE_SOILTEMP2F: ('Soil Temp 2', '°F', TYPE_SENSOR, 'temperature'),
TYPE_SOILTEMP3F: ('Soil Temp 3', '°F', TYPE_SENSOR, 'temperature'),
TYPE_SOILTEMP4F: ('Soil Temp 4', '°F', TYPE_SENSOR, 'temperature'),
TYPE_SOILTEMP5F: ('Soil Temp 5', '°F', TYPE_SENSOR, 'temperature'),
TYPE_SOILTEMP6F: ('Soil Temp 6', '°F', TYPE_SENSOR, 'temperature'),
TYPE_SOILTEMP7F: ('Soil Temp 7', '°F', TYPE_SENSOR, 'temperature'),
TYPE_SOILTEMP8F: ('Soil Temp 8', '°F', TYPE_SENSOR, 'temperature'),
TYPE_SOILTEMP9F: ('Soil Temp 9', '°F', TYPE_SENSOR, 'temperature'),
TYPE_SOLARRADIATION: ('Solar Rad', 'W/m^2', TYPE_SENSOR, None),
TYPE_SOLARRADIATION_LX: (
'Solar Rad (lx)', 'lx', TYPE_SENSOR, 'illuminance'),
TYPE_TEMP10F: ('Temp 10', '°F', TYPE_SENSOR, 'temperature'),
TYPE_TEMP1F: ('Temp 1', '°F', TYPE_SENSOR, 'temperature'),
TYPE_TEMP2F: ('Temp 2', '°F', TYPE_SENSOR, 'temperature'),
TYPE_TEMP3F: ('Temp 3', '°F', TYPE_SENSOR, 'temperature'),
TYPE_TEMP4F: ('Temp 4', '°F', TYPE_SENSOR, 'temperature'),
TYPE_TEMP5F: ('Temp 5', '°F', TYPE_SENSOR, 'temperature'),
TYPE_TEMP6F: ('Temp 6', '°F', TYPE_SENSOR, 'temperature'),
TYPE_TEMP7F: ('Temp 7', '°F', TYPE_SENSOR, 'temperature'),
TYPE_TEMP8F: ('Temp 8', '°F', TYPE_SENSOR, 'temperature'),
TYPE_TEMP9F: ('Temp 9', '°F', TYPE_SENSOR, 'temperature'),
TYPE_TEMPF: ('Temp', '°F', TYPE_SENSOR, 'temperature'),
TYPE_TEMPINF: ('Inside Temp', '°F', TYPE_SENSOR, 'temperature'),
TYPE_TOTALRAININ: ('Lifetime Rain', 'in', TYPE_SENSOR, None),
TYPE_UV: ('uv', 'Index', TYPE_SENSOR, None),
TYPE_WEEKLYRAININ: ('Weekly Rain', 'in', TYPE_SENSOR, None),
TYPE_WINDDIR: ('Wind Dir', '°', TYPE_SENSOR, None),
TYPE_WINDDIR_AVG10M: ('Wind Dir Avg 10m', '°', TYPE_SENSOR, None),
TYPE_WINDDIR_AVG2M: ('Wind Dir Avg 2m', 'mph', TYPE_SENSOR, None),
TYPE_WINDGUSTDIR: ('Gust Dir', '°', TYPE_SENSOR, None),
TYPE_WINDGUSTMPH: ('Wind Gust', 'mph', TYPE_SENSOR, None),
TYPE_WINDSPDMPH_AVG10M: ('Wind Avg 10m', 'mph', TYPE_SENSOR, None),
TYPE_WINDSPDMPH_AVG2M: ('Wind Avg 2m', 'mph', TYPE_SENSOR, None),
TYPE_WINDSPEEDMPH: ('Wind Speed', 'mph', TYPE_SENSOR, None),
TYPE_YEARLYRAININ: ('Yearly Rain', 'in', TYPE_SENSOR, None),
TYPE_24HOURRAININ: ("24 Hr Rain", "in", TYPE_SENSOR, None),
TYPE_BAROMABSIN: ("Abs Pressure", "inHg", TYPE_SENSOR, "pressure"),
TYPE_BAROMRELIN: ("Rel Pressure", "inHg", TYPE_SENSOR, "pressure"),
TYPE_BATT10: ("Battery 10", None, TYPE_BINARY_SENSOR, "battery"),
TYPE_BATT1: ("Battery 1", None, TYPE_BINARY_SENSOR, "battery"),
TYPE_BATT2: ("Battery 2", None, TYPE_BINARY_SENSOR, "battery"),
TYPE_BATT3: ("Battery 3", None, TYPE_BINARY_SENSOR, "battery"),
TYPE_BATT4: ("Battery 4", None, TYPE_BINARY_SENSOR, "battery"),
TYPE_BATT5: ("Battery 5", None, TYPE_BINARY_SENSOR, "battery"),
TYPE_BATT6: ("Battery 6", None, TYPE_BINARY_SENSOR, "battery"),
TYPE_BATT7: ("Battery 7", None, TYPE_BINARY_SENSOR, "battery"),
TYPE_BATT8: ("Battery 8", None, TYPE_BINARY_SENSOR, "battery"),
TYPE_BATT9: ("Battery 9", None, TYPE_BINARY_SENSOR, "battery"),
TYPE_BATTOUT: ("Battery", None, TYPE_BINARY_SENSOR, "battery"),
TYPE_CO2: ("co2", "ppm", TYPE_SENSOR, None),
TYPE_DAILYRAININ: ("Daily Rain", "in", TYPE_SENSOR, None),
TYPE_DEWPOINT: ("Dew Point", "°F", TYPE_SENSOR, "temperature"),
TYPE_EVENTRAININ: ("Event Rain", "in", TYPE_SENSOR, None),
TYPE_FEELSLIKE: ("Feels Like", "°F", TYPE_SENSOR, "temperature"),
TYPE_HOURLYRAININ: ("Hourly Rain Rate", "in/hr", TYPE_SENSOR, None),
TYPE_HUMIDITY10: ("Humidity 10", "%", TYPE_SENSOR, "humidity"),
TYPE_HUMIDITY1: ("Humidity 1", "%", TYPE_SENSOR, "humidity"),
TYPE_HUMIDITY2: ("Humidity 2", "%", TYPE_SENSOR, "humidity"),
TYPE_HUMIDITY3: ("Humidity 3", "%", TYPE_SENSOR, "humidity"),
TYPE_HUMIDITY4: ("Humidity 4", "%", TYPE_SENSOR, "humidity"),
TYPE_HUMIDITY5: ("Humidity 5", "%", TYPE_SENSOR, "humidity"),
TYPE_HUMIDITY6: ("Humidity 6", "%", TYPE_SENSOR, "humidity"),
TYPE_HUMIDITY7: ("Humidity 7", "%", TYPE_SENSOR, "humidity"),
TYPE_HUMIDITY8: ("Humidity 8", "%", TYPE_SENSOR, "humidity"),
TYPE_HUMIDITY9: ("Humidity 9", "%", TYPE_SENSOR, "humidity"),
TYPE_HUMIDITY: ("Humidity", "%", TYPE_SENSOR, "humidity"),
TYPE_HUMIDITYIN: ("Humidity In", "%", TYPE_SENSOR, "humidity"),
TYPE_LASTRAIN: ("Last Rain", None, TYPE_SENSOR, "timestamp"),
TYPE_MAXDAILYGUST: ("Max Gust", "mph", TYPE_SENSOR, None),
TYPE_MONTHLYRAININ: ("Monthly Rain", "in", TYPE_SENSOR, None),
TYPE_RELAY10: ("Relay 10", None, TYPE_BINARY_SENSOR, "connectivity"),
TYPE_RELAY1: ("Relay 1", None, TYPE_BINARY_SENSOR, "connectivity"),
TYPE_RELAY2: ("Relay 2", None, TYPE_BINARY_SENSOR, "connectivity"),
TYPE_RELAY3: ("Relay 3", None, TYPE_BINARY_SENSOR, "connectivity"),
TYPE_RELAY4: ("Relay 4", None, TYPE_BINARY_SENSOR, "connectivity"),
TYPE_RELAY5: ("Relay 5", None, TYPE_BINARY_SENSOR, "connectivity"),
TYPE_RELAY6: ("Relay 6", None, TYPE_BINARY_SENSOR, "connectivity"),
TYPE_RELAY7: ("Relay 7", None, TYPE_BINARY_SENSOR, "connectivity"),
TYPE_RELAY8: ("Relay 8", None, TYPE_BINARY_SENSOR, "connectivity"),
TYPE_RELAY9: ("Relay 9", None, TYPE_BINARY_SENSOR, "connectivity"),
TYPE_SOILHUM10: ("Soil Humidity 10", "%", TYPE_SENSOR, "humidity"),
TYPE_SOILHUM1: ("Soil Humidity 1", "%", TYPE_SENSOR, "humidity"),
TYPE_SOILHUM2: ("Soil Humidity 2", "%", TYPE_SENSOR, "humidity"),
TYPE_SOILHUM3: ("Soil Humidity 3", "%", TYPE_SENSOR, "humidity"),
TYPE_SOILHUM4: ("Soil Humidity 4", "%", TYPE_SENSOR, "humidity"),
TYPE_SOILHUM5: ("Soil Humidity 5", "%", TYPE_SENSOR, "humidity"),
TYPE_SOILHUM6: ("Soil Humidity 6", "%", TYPE_SENSOR, "humidity"),
TYPE_SOILHUM7: ("Soil Humidity 7", "%", TYPE_SENSOR, "humidity"),
TYPE_SOILHUM8: ("Soil Humidity 8", "%", TYPE_SENSOR, "humidity"),
TYPE_SOILHUM9: ("Soil Humidity 9", "%", TYPE_SENSOR, "humidity"),
TYPE_SOILTEMP10F: ("Soil Temp 10", "°F", TYPE_SENSOR, "temperature"),
TYPE_SOILTEMP1F: ("Soil Temp 1", "°F", TYPE_SENSOR, "temperature"),
TYPE_SOILTEMP2F: ("Soil Temp 2", "°F", TYPE_SENSOR, "temperature"),
TYPE_SOILTEMP3F: ("Soil Temp 3", "°F", TYPE_SENSOR, "temperature"),
TYPE_SOILTEMP4F: ("Soil Temp 4", "°F", TYPE_SENSOR, "temperature"),
TYPE_SOILTEMP5F: ("Soil Temp 5", "°F", TYPE_SENSOR, "temperature"),
TYPE_SOILTEMP6F: ("Soil Temp 6", "°F", TYPE_SENSOR, "temperature"),
TYPE_SOILTEMP7F: ("Soil Temp 7", "°F", TYPE_SENSOR, "temperature"),
TYPE_SOILTEMP8F: ("Soil Temp 8", "°F", TYPE_SENSOR, "temperature"),
TYPE_SOILTEMP9F: ("Soil Temp 9", "°F", TYPE_SENSOR, "temperature"),
TYPE_SOLARRADIATION: ("Solar Rad", "W/m^2", TYPE_SENSOR, None),
TYPE_SOLARRADIATION_LX: ("Solar Rad (lx)", "lx", TYPE_SENSOR, "illuminance"),
TYPE_TEMP10F: ("Temp 10", "°F", TYPE_SENSOR, "temperature"),
TYPE_TEMP1F: ("Temp 1", "°F", TYPE_SENSOR, "temperature"),
TYPE_TEMP2F: ("Temp 2", "°F", TYPE_SENSOR, "temperature"),
TYPE_TEMP3F: ("Temp 3", "°F", TYPE_SENSOR, "temperature"),
TYPE_TEMP4F: ("Temp 4", "°F", TYPE_SENSOR, "temperature"),
TYPE_TEMP5F: ("Temp 5", "°F", TYPE_SENSOR, "temperature"),
TYPE_TEMP6F: ("Temp 6", "°F", TYPE_SENSOR, "temperature"),
TYPE_TEMP7F: ("Temp 7", "°F", TYPE_SENSOR, "temperature"),
TYPE_TEMP8F: ("Temp 8", "°F", TYPE_SENSOR, "temperature"),
TYPE_TEMP9F: ("Temp 9", "°F", TYPE_SENSOR, "temperature"),
TYPE_TEMPF: ("Temp", "°F", TYPE_SENSOR, "temperature"),
TYPE_TEMPINF: ("Inside Temp", "°F", TYPE_SENSOR, "temperature"),
TYPE_TOTALRAININ: ("Lifetime Rain", "in", TYPE_SENSOR, None),
TYPE_UV: ("uv", "Index", TYPE_SENSOR, None),
TYPE_WEEKLYRAININ: ("Weekly Rain", "in", TYPE_SENSOR, None),
TYPE_WINDDIR: ("Wind Dir", "°", TYPE_SENSOR, None),
TYPE_WINDDIR_AVG10M: ("Wind Dir Avg 10m", "°", TYPE_SENSOR, None),
TYPE_WINDDIR_AVG2M: ("Wind Dir Avg 2m", "mph", TYPE_SENSOR, None),
TYPE_WINDGUSTDIR: ("Gust Dir", "°", TYPE_SENSOR, None),
TYPE_WINDGUSTMPH: ("Wind Gust", "mph", TYPE_SENSOR, None),
TYPE_WINDSPDMPH_AVG10M: ("Wind Avg 10m", "mph", TYPE_SENSOR, None),
TYPE_WINDSPDMPH_AVG2M: ("Wind Avg 2m", "mph", TYPE_SENSOR, None),
TYPE_WINDSPEEDMPH: ("Wind Speed", "mph", TYPE_SENSOR, None),
TYPE_YEARLYRAININ: ("Yearly Rain", "in", TYPE_SENSOR, None),
}
CONFIG_SCHEMA = vol.Schema({
DOMAIN:
vol.Schema({
vol.Required(CONF_APP_KEY): cv.string,
vol.Required(CONF_API_KEY): cv.string,
})
}, extra=vol.ALLOW_EXTRA)
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{
vol.Required(CONF_APP_KEY): cv.string,
vol.Required(CONF_API_KEY): cv.string,
}
)
},
extra=vol.ALLOW_EXTRA,
)
async def async_setup(hass, config):
@ -242,11 +257,10 @@ async def async_setup(hass, config):
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
context={'source': SOURCE_IMPORT},
data={
CONF_API_KEY: conf[CONF_API_KEY],
CONF_APP_KEY: conf[CONF_APP_KEY]
}))
context={"source": SOURCE_IMPORT},
data={CONF_API_KEY: conf[CONF_API_KEY], CONF_APP_KEY: conf[CONF_APP_KEY]},
)
)
return True
@ -257,18 +271,23 @@ async def async_setup_entry(hass, config_entry):
try:
ambient = AmbientStation(
hass, config_entry,
hass,
config_entry,
Client(
config_entry.data[CONF_API_KEY],
config_entry.data[CONF_APP_KEY], session))
config_entry.data[CONF_APP_KEY],
session,
),
)
hass.loop.create_task(ambient.ws_connect())
hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = ambient
except WebsocketError as err:
_LOGGER.error('Config entry failed: %s', err)
_LOGGER.error("Config entry failed: %s", err)
raise ConfigEntryNotReady
hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STOP, ambient.client.websocket.disconnect())
EVENT_HOMEASSISTANT_STOP, ambient.client.websocket.disconnect()
)
return True
@ -278,9 +297,8 @@ async def async_unload_entry(hass, config_entry):
ambient = hass.data[DOMAIN][DATA_CLIENT].pop(config_entry.entry_id)
hass.async_create_task(ambient.ws_disconnect())
for component in ('binary_sensor', 'sensor'):
await hass.config_entries.async_forward_entry_unload(
config_entry, component)
for component in ("binary_sensor", "sensor"):
await hass.config_entries.async_forward_entry_unload(config_entry, component)
return True
@ -289,7 +307,7 @@ async def async_migrate_entry(hass, config_entry):
"""Migrate old entry."""
version = config_entry.version
_LOGGER.debug('Migrating from version %s', version)
_LOGGER.debug("Migrating from version %s", version)
# 1 -> 2: Unique ID format changed, so delete and re-import:
if version == 1:
@ -302,7 +320,7 @@ async def async_migrate_entry(hass, config_entry):
version = config_entry.version = 2
hass.config_entries.async_update_entry(config_entry)
_LOGGER.info('Migration to version %s successful', version)
_LOGGER.info("Migration to version %s successful", version)
return True
@ -327,71 +345,70 @@ class AmbientStation:
await self.client.websocket.connect()
except WebsocketError as err:
_LOGGER.error("Error with the websocket connection: %s", err)
self._ws_reconnect_delay = min(
2 * self._ws_reconnect_delay, 480)
async_call_later(
self._hass, self._ws_reconnect_delay, self.ws_connect)
self._ws_reconnect_delay = min(2 * self._ws_reconnect_delay, 480)
async_call_later(self._hass, self._ws_reconnect_delay, self.ws_connect)
async def ws_connect(self):
"""Register handlers and connect to the websocket."""
async def _ws_reconnect(event_time):
"""Forcibly disconnect from and reconnect to the websocket."""
_LOGGER.debug('Watchdog expired; forcing socket reconnection')
_LOGGER.debug("Watchdog expired; forcing socket reconnection")
await self.client.websocket.disconnect()
await self._attempt_connect()
def on_connect():
"""Define a handler to fire when the websocket is connected."""
_LOGGER.info('Connected to websocket')
_LOGGER.debug('Watchdog starting')
_LOGGER.info("Connected to websocket")
_LOGGER.debug("Watchdog starting")
if self._watchdog_listener is not None:
self._watchdog_listener()
self._watchdog_listener = async_call_later(
self._hass, DEFAULT_WATCHDOG_SECONDS, _ws_reconnect)
self._hass, DEFAULT_WATCHDOG_SECONDS, _ws_reconnect
)
def on_data(data):
"""Define a handler to fire when the data is received."""
mac_address = data['macAddress']
mac_address = data["macAddress"]
if data != self.stations[mac_address][ATTR_LAST_DATA]:
_LOGGER.debug('New data received: %s', data)
_LOGGER.debug("New data received: %s", data)
self.stations[mac_address][ATTR_LAST_DATA] = data
async_dispatcher_send(self._hass, TOPIC_UPDATE)
_LOGGER.debug('Resetting watchdog')
_LOGGER.debug("Resetting watchdog")
self._watchdog_listener()
self._watchdog_listener = async_call_later(
self._hass, DEFAULT_WATCHDOG_SECONDS, _ws_reconnect)
self._hass, DEFAULT_WATCHDOG_SECONDS, _ws_reconnect
)
def on_disconnect():
"""Define a handler to fire when the websocket is disconnected."""
_LOGGER.info('Disconnected from websocket')
_LOGGER.info("Disconnected from websocket")
def on_subscribed(data):
"""Define a handler to fire when the subscription is set."""
for station in data['devices']:
if station['macAddress'] in self.stations:
for station in data["devices"]:
if station["macAddress"] in self.stations:
continue
_LOGGER.debug('New station subscription: %s', data)
_LOGGER.debug("New station subscription: %s", data)
self.monitored_conditions = [
k for k in station['lastData']
if k in SENSOR_TYPES
k for k in station["lastData"] if k in SENSOR_TYPES
]
# If the user is monitoring brightness (in W/m^2),
# make sure we also add a calculated sensor for the
# same data measured in lx:
if TYPE_SOLARRADIATION in self.monitored_conditions:
self.monitored_conditions.append(
TYPE_SOLARRADIATION_LX)
self.monitored_conditions.append(TYPE_SOLARRADIATION_LX)
self.stations[station['macAddress']] = {
ATTR_LAST_DATA: station['lastData'],
ATTR_LOCATION: station.get('info', {}).get('location'),
ATTR_NAME:
station.get('info', {}).get(
'name', station['macAddress']),
self.stations[station["macAddress"]] = {
ATTR_LAST_DATA: station["lastData"],
ATTR_LOCATION: station.get("info", {}).get("location"),
ATTR_NAME: station.get("info", {}).get(
"name", station["macAddress"]
),
}
# If the websocket disconnects and reconnects, the on_subscribed
@ -399,10 +416,12 @@ class AmbientStation:
# attempt forward setup of the config entry (because it will have
# already been done):
if not self._entry_setup_complete:
for component in ('binary_sensor', 'sensor'):
for component in ("binary_sensor", "sensor"):
self._hass.async_create_task(
self._hass.config_entries.async_forward_entry_setup(
self._config_entry, component))
self._config_entry, component
)
)
self._entry_setup_complete = True
self._ws_reconnect_delay = DEFAULT_SOCKET_MIN_RETRY
@ -423,8 +442,8 @@ class AmbientWeatherEntity(Entity):
"""Define a base Ambient PWS entity."""
def __init__(
self, ambient, mac_address, station_name, sensor_type,
sensor_name, device_class):
self, ambient, mac_address, station_name, sensor_type, sensor_name, device_class
):
"""Initialize the sensor."""
self._ambient = ambient
self._device_class = device_class
@ -443,10 +462,18 @@ class AmbientWeatherEntity(Entity):
# solarradiation_lx sensor shows as available if the solarradiation
# sensor is available:
if self._sensor_type == TYPE_SOLARRADIATION_LX:
return self._ambient.stations[self._mac_address][
ATTR_LAST_DATA].get(TYPE_SOLARRADIATION) is not None
return self._ambient.stations[self._mac_address][ATTR_LAST_DATA].get(
self._sensor_type) is not None
return (
self._ambient.stations[self._mac_address][ATTR_LAST_DATA].get(
TYPE_SOLARRADIATION
)
is not None
)
return (
self._ambient.stations[self._mac_address][ATTR_LAST_DATA].get(
self._sensor_type
)
is not None
)
@property
def device_class(self):
@ -457,17 +484,15 @@ class AmbientWeatherEntity(Entity):
def device_info(self):
"""Return device registry information for this entity."""
return {
'identifiers': {
(DOMAIN, self._mac_address)
},
'name': self._station_name,
'manufacturer': 'Ambient Weather',
"identifiers": {(DOMAIN, self._mac_address)},
"name": self._station_name,
"manufacturer": "Ambient Weather",
}
@property
def name(self):
"""Return the name of the sensor."""
return '{0}_{1}'.format(self._station_name, self._sensor_name)
return "{0}_{1}".format(self._station_name, self._sensor_name)
@property
def should_poll(self):
@ -477,17 +502,19 @@ class AmbientWeatherEntity(Entity):
@property
def unique_id(self):
"""Return a unique, unchanging string that represents this sensor."""
return '{0}_{1}'.format(self._mac_address, self._sensor_type)
return "{0}_{1}".format(self._mac_address, self._sensor_type)
async def async_added_to_hass(self):
"""Register callbacks."""
@callback
def update():
"""Update the state."""
self.async_schedule_update_ha_state(True)
self._async_unsub_dispatcher_connect = async_dispatcher_connect(
self.hass, TOPIC_UPDATE, update)
self.hass, TOPIC_UPDATE, update
)
async def async_will_remove_from_hass(self):
"""Disconnect dispatcher listener when removed."""

View file

@ -5,16 +5,26 @@ from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.const import ATTR_NAME
from . import (
SENSOR_TYPES, TYPE_BATT1, TYPE_BATT2, TYPE_BATT3, TYPE_BATT4, TYPE_BATT5,
TYPE_BATT6, TYPE_BATT7, TYPE_BATT8, TYPE_BATT9, TYPE_BATT10, TYPE_BATTOUT,
AmbientWeatherEntity)
SENSOR_TYPES,
TYPE_BATT1,
TYPE_BATT2,
TYPE_BATT3,
TYPE_BATT4,
TYPE_BATT5,
TYPE_BATT6,
TYPE_BATT7,
TYPE_BATT8,
TYPE_BATT9,
TYPE_BATT10,
TYPE_BATTOUT,
AmbientWeatherEntity,
)
from .const import ATTR_LAST_DATA, DATA_CLIENT, DOMAIN, TYPE_BINARY_SENSOR
_LOGGER = logging.getLogger(__name__)
async def async_setup_platform(
hass, config, async_add_entities, discovery_info=None):
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up Ambient PWS binary sensors based on the old way."""
pass
@ -30,8 +40,14 @@ async def async_setup_entry(hass, entry, async_add_entities):
if kind == TYPE_BINARY_SENSOR:
binary_sensor_list.append(
AmbientWeatherBinarySensor(
ambient, mac_address, station[ATTR_NAME], condition,
name, device_class))
ambient,
mac_address,
station[ATTR_NAME],
condition,
name,
device_class,
)
)
async_add_entities(binary_sensor_list, True)
@ -42,15 +58,25 @@ class AmbientWeatherBinarySensor(AmbientWeatherEntity, BinarySensorDevice):
@property
def is_on(self):
"""Return the status of the sensor."""
if self._sensor_type in (TYPE_BATT1, TYPE_BATT10, TYPE_BATT2,
TYPE_BATT3, TYPE_BATT4, TYPE_BATT5,
TYPE_BATT6, TYPE_BATT7, TYPE_BATT8,
TYPE_BATT9, TYPE_BATTOUT):
if self._sensor_type in (
TYPE_BATT1,
TYPE_BATT10,
TYPE_BATT2,
TYPE_BATT3,
TYPE_BATT4,
TYPE_BATT5,
TYPE_BATT6,
TYPE_BATT7,
TYPE_BATT8,
TYPE_BATT9,
TYPE_BATTOUT,
):
return self._state == 0
return self._state == 1
async def async_update(self):
"""Fetch new state data for the entity."""
self._state = self._ambient.stations[
self._mac_address][ATTR_LAST_DATA].get(self._sensor_type)
self._state = self._ambient.stations[self._mac_address][ATTR_LAST_DATA].get(
self._sensor_type
)

View file

@ -13,8 +13,8 @@ from .const import CONF_APP_KEY, DOMAIN
def configured_instances(hass):
"""Return a set of configured Ambient PWS instances."""
return set(
entry.data[CONF_APP_KEY]
for entry in hass.config_entries.async_entries(DOMAIN))
entry.data[CONF_APP_KEY] for entry in hass.config_entries.async_entries(DOMAIN)
)
@config_entries.HANDLERS.register(DOMAIN)
@ -26,15 +26,12 @@ class AmbientStationFlowHandler(config_entries.ConfigFlow):
async def _show_form(self, errors=None):
"""Show the form to the user."""
data_schema = vol.Schema({
vol.Required(CONF_API_KEY): str,
vol.Required(CONF_APP_KEY): str,
})
data_schema = vol.Schema(
{vol.Required(CONF_API_KEY): str, vol.Required(CONF_APP_KEY): str}
)
return self.async_show_form(
step_id='user',
data_schema=data_schema,
errors=errors if errors else {},
step_id="user", data_schema=data_schema, errors=errors if errors else {}
)
async def async_step_import(self, import_config):
@ -50,22 +47,22 @@ class AmbientStationFlowHandler(config_entries.ConfigFlow):
return await self._show_form()
if user_input[CONF_APP_KEY] in configured_instances(self.hass):
return await self._show_form({CONF_APP_KEY: 'identifier_exists'})
return await self._show_form({CONF_APP_KEY: "identifier_exists"})
session = aiohttp_client.async_get_clientsession(self.hass)
client = Client(
user_input[CONF_API_KEY], user_input[CONF_APP_KEY], session)
client = Client(user_input[CONF_API_KEY], user_input[CONF_APP_KEY], session)
try:
devices = await client.api.get_devices()
except AmbientError:
return await self._show_form({'base': 'invalid_key'})
return await self._show_form({"base": "invalid_key"})
if not devices:
return await self._show_form({'base': 'no_devices'})
return await self._show_form({"base": "no_devices"})
# The Application Key (which identifies each config entry) is too long
# to show nicely in the UI, so we take the first 12 characters (similar
# to how GitHub does it):
return self.async_create_entry(
title=user_input[CONF_APP_KEY][:12], data=user_input)
title=user_input[CONF_APP_KEY][:12], data=user_input
)

View file

@ -1,13 +1,13 @@
"""Define constants for the Ambient PWS component."""
DOMAIN = 'ambient_station'
DOMAIN = "ambient_station"
ATTR_LAST_DATA = 'last_data'
ATTR_LAST_DATA = "last_data"
CONF_APP_KEY = 'app_key'
CONF_APP_KEY = "app_key"
DATA_CLIENT = 'data_client'
DATA_CLIENT = "data_client"
TOPIC_UPDATE = 'update'
TOPIC_UPDATE = "update"
TYPE_BINARY_SENSOR = 'binary_sensor'
TYPE_SENSOR = 'sensor'
TYPE_BINARY_SENSOR = "binary_sensor"
TYPE_SENSOR = "sensor"

View file

@ -4,15 +4,17 @@ import logging
from homeassistant.const import ATTR_NAME
from . import (
SENSOR_TYPES, TYPE_SOLARRADIATION, TYPE_SOLARRADIATION_LX,
AmbientWeatherEntity)
SENSOR_TYPES,
TYPE_SOLARRADIATION,
TYPE_SOLARRADIATION_LX,
AmbientWeatherEntity,
)
from .const import ATTR_LAST_DATA, DATA_CLIENT, DOMAIN, TYPE_SENSOR
_LOGGER = logging.getLogger(__name__)
async def async_setup_platform(
hass, config, async_add_entities, discovery_info=None):
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up Ambient PWS sensors based on existing config."""
pass
@ -28,8 +30,15 @@ async def async_setup_entry(hass, entry, async_add_entities):
if kind == TYPE_SENSOR:
sensor_list.append(
AmbientWeatherSensor(
ambient, mac_address, station[ATTR_NAME], condition,
name, device_class, unit))
ambient,
mac_address,
station[ATTR_NAME],
condition,
name,
device_class,
unit,
)
)
async_add_entities(sensor_list, True)
@ -38,16 +47,19 @@ class AmbientWeatherSensor(AmbientWeatherEntity):
"""Define an Ambient sensor."""
def __init__(
self, ambient, mac_address, station_name, sensor_type, sensor_name,
device_class, unit):
self,
ambient,
mac_address,
station_name,
sensor_type,
sensor_name,
device_class,
unit,
):
"""Initialize the sensor."""
super().__init__(
ambient,
mac_address,
station_name,
sensor_type,
sensor_name,
device_class)
ambient, mac_address, station_name, sensor_type, sensor_name, device_class
)
self._unit = unit
@ -67,9 +79,11 @@ class AmbientWeatherSensor(AmbientWeatherEntity):
# If the user requests the solarradiation_lx sensor, use the
# value of the solarradiation sensor and apply a very accurate
# approximation of converting sunlight W/m^2 to lx:
w_m2_brightness_val = self._ambient.stations[
self._mac_address][ATTR_LAST_DATA].get(TYPE_SOLARRADIATION)
self._state = round(float(w_m2_brightness_val)/0.0079)
w_m2_brightness_val = self._ambient.stations[self._mac_address][
ATTR_LAST_DATA
].get(TYPE_SOLARRADIATION)
self._state = round(float(w_m2_brightness_val) / 0.0079)
else:
self._state = self._ambient.stations[
self._mac_address][ATTR_LAST_DATA].get(self._sensor_type)
self._state = self._ambient.stations[self._mac_address][ATTR_LAST_DATA].get(
self._sensor_type
)

View file

@ -13,14 +13,24 @@ from homeassistant.components.camera import DOMAIN as CAMERA
from homeassistant.components.sensor import DOMAIN as SENSOR
from homeassistant.components.switch import DOMAIN as SWITCH
from homeassistant.const import (
ATTR_ENTITY_ID, CONF_AUTHENTICATION, CONF_BINARY_SENSORS, CONF_HOST,
CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_SCAN_INTERVAL, CONF_SENSORS,
CONF_SWITCHES, CONF_USERNAME, ENTITY_MATCH_ALL, HTTP_BASIC_AUTHENTICATION)
ATTR_ENTITY_ID,
CONF_AUTHENTICATION,
CONF_BINARY_SENSORS,
CONF_HOST,
CONF_NAME,
CONF_PASSWORD,
CONF_PORT,
CONF_SCAN_INTERVAL,
CONF_SENSORS,
CONF_SWITCHES,
CONF_USERNAME,
ENTITY_MATCH_ALL,
HTTP_BASIC_AUTHENTICATION,
)
from homeassistant.exceptions import Unauthorized, UnknownUser
from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import (
async_dispatcher_send, dispatcher_send)
from homeassistant.helpers.dispatcher import async_dispatcher_send, dispatcher_send
from homeassistant.helpers.event import track_time_interval
from homeassistant.helpers.service import async_extract_entity_ids
@ -33,31 +43,26 @@ from .switch import SWITCHES
_LOGGER = logging.getLogger(__name__)
CONF_RESOLUTION = 'resolution'
CONF_STREAM_SOURCE = 'stream_source'
CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments'
CONF_CONTROL_LIGHT = 'control_light'
CONF_RESOLUTION = "resolution"
CONF_STREAM_SOURCE = "stream_source"
CONF_FFMPEG_ARGUMENTS = "ffmpeg_arguments"
CONF_CONTROL_LIGHT = "control_light"
DEFAULT_NAME = 'Amcrest Camera'
DEFAULT_NAME = "Amcrest Camera"
DEFAULT_PORT = 80
DEFAULT_RESOLUTION = 'high'
DEFAULT_ARGUMENTS = '-pred 1'
DEFAULT_RESOLUTION = "high"
DEFAULT_ARGUMENTS = "-pred 1"
MAX_ERRORS = 5
RECHECK_INTERVAL = timedelta(minutes=1)
NOTIFICATION_ID = 'amcrest_notification'
NOTIFICATION_TITLE = 'Amcrest Camera Setup'
NOTIFICATION_ID = "amcrest_notification"
NOTIFICATION_TITLE = "Amcrest Camera Setup"
RESOLUTION_LIST = {
'high': 0,
'low': 1,
}
RESOLUTION_LIST = {"high": 0, "low": 1}
SCAN_INTERVAL = timedelta(seconds=10)
AUTHENTICATION_LIST = {
'basic': 'basic'
}
AUTHENTICATION_LIST = {"basic": "basic"}
def _deprecated_sensor_values(sensors):
@ -66,8 +71,11 @@ def _deprecated_sensor_values(sensors):
"The '%s' option value '%s' is deprecated, "
"please remove it from your configuration and use "
"the '%s' option with value '%s' instead",
CONF_SENSORS, SENSOR_MOTION_DETECTOR, CONF_BINARY_SENSORS,
BINARY_SENSOR_MOTION_DETECTED)
CONF_SENSORS,
SENSOR_MOTION_DETECTOR,
CONF_BINARY_SENSORS,
BINARY_SENSOR_MOTION_DETECTED,
)
return sensors
@ -77,7 +85,9 @@ def _deprecated_switches(config):
"The '%s' option (with value %s) is deprecated, "
"please remove it from your configuration and use "
"services and attributes instead",
CONF_SWITCHES, config[CONF_SWITCHES])
CONF_SWITCHES,
config[CONF_SWITCHES],
)
return config
@ -88,37 +98,41 @@ def _has_unique_names(devices):
AMCREST_SCHEMA = vol.All(
vol.Schema({
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_AUTHENTICATION, default=HTTP_BASIC_AUTHENTICATION):
vol.All(vol.In(AUTHENTICATION_LIST)),
vol.Optional(CONF_RESOLUTION, default=DEFAULT_RESOLUTION):
vol.All(vol.In(RESOLUTION_LIST)),
vol.Optional(CONF_STREAM_SOURCE, default=STREAM_SOURCE_LIST[0]):
vol.All(vol.In(STREAM_SOURCE_LIST)),
vol.Optional(CONF_FFMPEG_ARGUMENTS, default=DEFAULT_ARGUMENTS):
cv.string,
vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL):
cv.time_period,
vol.Optional(CONF_BINARY_SENSORS):
vol.All(cv.ensure_list, [vol.In(BINARY_SENSORS)]),
vol.Optional(CONF_SENSORS):
vol.All(cv.ensure_list, [vol.In(SENSORS)],
_deprecated_sensor_values),
vol.Optional(CONF_SWITCHES):
vol.All(cv.ensure_list, [vol.In(SWITCHES)]),
vol.Optional(CONF_CONTROL_LIGHT, default=True): cv.boolean,
}),
_deprecated_switches
vol.Schema(
{
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(
CONF_AUTHENTICATION, default=HTTP_BASIC_AUTHENTICATION
): vol.All(vol.In(AUTHENTICATION_LIST)),
vol.Optional(CONF_RESOLUTION, default=DEFAULT_RESOLUTION): vol.All(
vol.In(RESOLUTION_LIST)
),
vol.Optional(CONF_STREAM_SOURCE, default=STREAM_SOURCE_LIST[0]): vol.All(
vol.In(STREAM_SOURCE_LIST)
),
vol.Optional(CONF_FFMPEG_ARGUMENTS, default=DEFAULT_ARGUMENTS): cv.string,
vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): cv.time_period,
vol.Optional(CONF_BINARY_SENSORS): vol.All(
cv.ensure_list, [vol.In(BINARY_SENSORS)]
),
vol.Optional(CONF_SENSORS): vol.All(
cv.ensure_list, [vol.In(SENSORS)], _deprecated_sensor_values
),
vol.Optional(CONF_SWITCHES): vol.All(cv.ensure_list, [vol.In(SWITCHES)]),
vol.Optional(CONF_CONTROL_LIGHT, default=True): cv.boolean,
}
),
_deprecated_switches,
)
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.All(cv.ensure_list, [AMCREST_SCHEMA], _has_unique_names)
}, extra=vol.ALLOW_EXTRA)
CONFIG_SCHEMA = vol.Schema(
{DOMAIN: vol.All(cv.ensure_list, [AMCREST_SCHEMA], _has_unique_names)},
extra=vol.ALLOW_EXTRA,
)
# pylint: disable=too-many-ancestors
@ -132,8 +146,9 @@ class AmcrestChecker(Http):
self._wrap_errors = 0
self._wrap_lock = threading.Lock()
self._unsub_recheck = None
super().__init__(host, port, user, password, retries_connection=1,
timeout_protocol=3.05)
super().__init__(
host, port, user, password, retries_connection=1, timeout_protocol=3.05
)
@property
def available(self):
@ -148,17 +163,16 @@ class AmcrestChecker(Http):
with self._wrap_lock:
was_online = self.available
self._wrap_errors += 1
_LOGGER.debug('%s camera errs: %i', self._wrap_name,
self._wrap_errors)
_LOGGER.debug("%s camera errs: %i", self._wrap_name, self._wrap_errors)
offline = not self.available
if offline and was_online:
_LOGGER.error(
'%s camera offline: Too many errors', self._wrap_name)
_LOGGER.error("%s camera offline: Too many errors", self._wrap_name)
dispatcher_send(
self._hass,
service_signal(SERVICE_UPDATE, self._wrap_name))
self._hass, service_signal(SERVICE_UPDATE, self._wrap_name)
)
self._unsub_recheck = track_time_interval(
self._hass, self._wrap_test_online, RECHECK_INTERVAL)
self._hass, self._wrap_test_online, RECHECK_INTERVAL
)
raise
with self._wrap_lock:
was_offline = not self.available
@ -166,9 +180,8 @@ class AmcrestChecker(Http):
if was_offline:
self._unsub_recheck()
self._unsub_recheck = None
_LOGGER.error('%s camera back online', self._wrap_name)
dispatcher_send(
self._hass, service_signal(SERVICE_UPDATE, self._wrap_name))
_LOGGER.error("%s camera back online", self._wrap_name)
dispatcher_send(self._hass, service_signal(SERVICE_UPDATE, self._wrap_name))
return ret
def _wrap_test_online(self, now):
@ -190,9 +203,8 @@ def setup(hass, config):
try:
api = AmcrestChecker(
hass, name,
device[CONF_HOST], device[CONF_PORT],
username, password)
hass, name, device[CONF_HOST], device[CONF_PORT], username, password
)
except LoginError as ex:
_LOGGER.error("Login error for %s camera: %s", name, ex)
@ -214,41 +226,40 @@ def setup(hass, config):
authentication = None
hass.data[DATA_AMCREST][DEVICES][name] = AmcrestDevice(
api, authentication, ffmpeg_arguments, stream_source,
resolution, control_light)
api,
authentication,
ffmpeg_arguments,
stream_source,
resolution,
control_light,
)
discovery.load_platform(
hass, CAMERA, DOMAIN, {
CONF_NAME: name,
}, config)
discovery.load_platform(hass, CAMERA, DOMAIN, {CONF_NAME: name}, config)
if binary_sensors:
discovery.load_platform(
hass, BINARY_SENSOR, DOMAIN, {
CONF_NAME: name,
CONF_BINARY_SENSORS: binary_sensors
}, config)
hass,
BINARY_SENSOR,
DOMAIN,
{CONF_NAME: name, CONF_BINARY_SENSORS: binary_sensors},
config,
)
if sensors:
discovery.load_platform(
hass, SENSOR, DOMAIN, {
CONF_NAME: name,
CONF_SENSORS: sensors,
}, config)
hass, SENSOR, DOMAIN, {CONF_NAME: name, CONF_SENSORS: sensors}, config
)
if switches:
discovery.load_platform(
hass, SWITCH, DOMAIN, {
CONF_NAME: name,
CONF_SWITCHES: switches
}, config)
hass, SWITCH, DOMAIN, {CONF_NAME: name, CONF_SWITCHES: switches}, config
)
if not hass.data[DATA_AMCREST][DEVICES]:
return False
def have_permission(user, entity_id):
return not user or user.permissions.check_entity(
entity_id, POLICY_CONTROL)
return not user or user.permissions.check_entity(entity_id, POLICY_CONTROL)
async def async_extract_from_service(call):
if call.context.user_id:
@ -261,7 +272,8 @@ def setup(hass, config):
if call.data.get(ATTR_ENTITY_ID) == ENTITY_MATCH_ALL:
# Return all entity_ids user has permission to control.
return [
entity_id for entity_id in hass.data[DATA_AMCREST][CAMERAS]
entity_id
for entity_id in hass.data[DATA_AMCREST][CAMERAS]
if have_permission(user, entity_id)
]
@ -272,9 +284,7 @@ def setup(hass, config):
continue
if not have_permission(user, entity_id):
raise Unauthorized(
context=call.context,
entity_id=entity_id,
permission=POLICY_CONTROL
context=call.context, entity_id=entity_id, permission=POLICY_CONTROL
)
entity_ids.append(entity_id)
return entity_ids
@ -284,15 +294,10 @@ def setup(hass, config):
for arg in CAMERA_SERVICES[call.service][2]:
args.append(call.data[arg])
for entity_id in await async_extract_from_service(call):
async_dispatcher_send(
hass,
service_signal(call.service, entity_id),
*args
)
async_dispatcher_send(hass, service_signal(call.service, entity_id), *args)
for service, params in CAMERA_SERVICES.items():
hass.services.async_register(
DOMAIN, service, async_service_handler, params[0])
hass.services.async_register(DOMAIN, service, async_service_handler, params[0])
return True
@ -300,8 +305,15 @@ def setup(hass, config):
class AmcrestDevice:
"""Representation of a base Amcrest discovery device."""
def __init__(self, api, authentication, ffmpeg_arguments,
stream_source, resolution, control_light):
def __init__(
self,
api,
authentication,
ffmpeg_arguments,
stream_source,
resolution,
control_light,
):
"""Initialize the entity."""
self.api = api
self.authentication = authentication

View file

@ -5,29 +5,35 @@ import logging
from amcrest import AmcrestError
from homeassistant.components.binary_sensor import (
BinarySensorDevice, DEVICE_CLASS_CONNECTIVITY, DEVICE_CLASS_MOTION)
BinarySensorDevice,
DEVICE_CLASS_CONNECTIVITY,
DEVICE_CLASS_MOTION,
)
from homeassistant.const import CONF_NAME, CONF_BINARY_SENSORS
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .const import (
BINARY_SENSOR_SCAN_INTERVAL_SECS, DATA_AMCREST, DEVICES, SERVICE_UPDATE)
BINARY_SENSOR_SCAN_INTERVAL_SECS,
DATA_AMCREST,
DEVICES,
SERVICE_UPDATE,
)
from .helpers import log_update_error, service_signal
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=BINARY_SENSOR_SCAN_INTERVAL_SECS)
BINARY_SENSOR_MOTION_DETECTED = 'motion_detected'
BINARY_SENSOR_ONLINE = 'online'
BINARY_SENSOR_MOTION_DETECTED = "motion_detected"
BINARY_SENSOR_ONLINE = "online"
# Binary sensor types are defined like: Name, device class
BINARY_SENSORS = {
BINARY_SENSOR_MOTION_DETECTED: ('Motion Detected', DEVICE_CLASS_MOTION),
BINARY_SENSOR_ONLINE: ('Online', DEVICE_CLASS_CONNECTIVITY),
BINARY_SENSOR_MOTION_DETECTED: ("Motion Detected", DEVICE_CLASS_MOTION),
BINARY_SENSOR_ONLINE: ("Online", DEVICE_CLASS_CONNECTIVITY),
}
async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up a binary sensor for an Amcrest IP Camera."""
if discovery_info is None:
return
@ -35,9 +41,12 @@ async def async_setup_platform(hass, config, async_add_entities,
name = discovery_info[CONF_NAME]
device = hass.data[DATA_AMCREST][DEVICES][name]
async_add_entities(
[AmcrestBinarySensor(name, device, sensor_type)
for sensor_type in discovery_info[CONF_BINARY_SENSORS]],
True)
[
AmcrestBinarySensor(name, device, sensor_type)
for sensor_type in discovery_info[CONF_BINARY_SENSORS]
],
True,
)
class AmcrestBinarySensor(BinarySensorDevice):
@ -45,7 +54,7 @@ class AmcrestBinarySensor(BinarySensorDevice):
def __init__(self, name, device, sensor_type):
"""Initialize entity."""
self._name = '{} {}'.format(name, BINARY_SENSORS[sensor_type][0])
self._name = "{} {}".format(name, BINARY_SENSORS[sensor_type][0])
self._signal_name = name
self._api = device.api
self._sensor_type = sensor_type
@ -82,7 +91,7 @@ class AmcrestBinarySensor(BinarySensorDevice):
"""Update entity."""
if not self.available:
return
_LOGGER.debug('Updating %s binary sensor', self._name)
_LOGGER.debug("Updating %s binary sensor", self._name)
try:
if self._sensor_type == BINARY_SENSOR_MOTION_DETECTED:
@ -91,8 +100,7 @@ class AmcrestBinarySensor(BinarySensorDevice):
elif self._sensor_type == BINARY_SENSOR_ONLINE:
self._state = self._api.available
except AmcrestError as error:
log_update_error(
_LOGGER, 'update', self.name, 'binary sensor', error)
log_update_error(_LOGGER, "update", self.name, "binary sensor", error)
async def async_on_demand_update(self):
"""Update state."""
@ -101,8 +109,10 @@ class AmcrestBinarySensor(BinarySensorDevice):
async def async_added_to_hass(self):
"""Subscribe to update signal."""
self._unsub_dispatcher = async_dispatcher_connect(
self.hass, service_signal(SERVICE_UPDATE, self._signal_name),
self.async_on_demand_update)
self.hass,
service_signal(SERVICE_UPDATE, self._signal_name),
self.async_on_demand_update,
)
async def async_will_remove_from_hass(self):
"""Disconnect from update signal."""

View file

@ -8,83 +8,85 @@ from amcrest import AmcrestError
import voluptuous as vol
from homeassistant.components.camera import (
Camera, CAMERA_SERVICE_SCHEMA, SUPPORT_ON_OFF, SUPPORT_STREAM)
Camera,
CAMERA_SERVICE_SCHEMA,
SUPPORT_ON_OFF,
SUPPORT_STREAM,
)
from homeassistant.components.ffmpeg import DATA_FFMPEG
from homeassistant.const import (
CONF_NAME, STATE_ON, STATE_OFF)
from homeassistant.const import CONF_NAME, STATE_ON, STATE_OFF
from homeassistant.helpers.aiohttp_client import (
async_aiohttp_proxy_stream, async_aiohttp_proxy_web,
async_get_clientsession)
async_aiohttp_proxy_stream,
async_aiohttp_proxy_web,
async_get_clientsession,
)
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .const import (
CAMERA_WEB_SESSION_TIMEOUT, CAMERAS, DATA_AMCREST, DEVICES, SERVICE_UPDATE)
CAMERA_WEB_SESSION_TIMEOUT,
CAMERAS,
DATA_AMCREST,
DEVICES,
SERVICE_UPDATE,
)
from .helpers import log_update_error, service_signal
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=15)
STREAM_SOURCE_LIST = [
'snapshot',
'mjpeg',
'rtsp',
]
STREAM_SOURCE_LIST = ["snapshot", "mjpeg", "rtsp"]
_SRV_EN_REC = 'enable_recording'
_SRV_DS_REC = 'disable_recording'
_SRV_EN_AUD = 'enable_audio'
_SRV_DS_AUD = 'disable_audio'
_SRV_EN_MOT_REC = 'enable_motion_recording'
_SRV_DS_MOT_REC = 'disable_motion_recording'
_SRV_GOTO = 'goto_preset'
_SRV_CBW = 'set_color_bw'
_SRV_TOUR_ON = 'start_tour'
_SRV_TOUR_OFF = 'stop_tour'
_SRV_EN_REC = "enable_recording"
_SRV_DS_REC = "disable_recording"
_SRV_EN_AUD = "enable_audio"
_SRV_DS_AUD = "disable_audio"
_SRV_EN_MOT_REC = "enable_motion_recording"
_SRV_DS_MOT_REC = "disable_motion_recording"
_SRV_GOTO = "goto_preset"
_SRV_CBW = "set_color_bw"
_SRV_TOUR_ON = "start_tour"
_SRV_TOUR_OFF = "stop_tour"
_ATTR_PRESET = 'preset'
_ATTR_COLOR_BW = 'color_bw'
_ATTR_PRESET = "preset"
_ATTR_COLOR_BW = "color_bw"
_CBW_COLOR = 'color'
_CBW_AUTO = 'auto'
_CBW_BW = 'bw'
_CBW_COLOR = "color"
_CBW_AUTO = "auto"
_CBW_BW = "bw"
_CBW = [_CBW_COLOR, _CBW_AUTO, _CBW_BW]
_SRV_GOTO_SCHEMA = CAMERA_SERVICE_SCHEMA.extend({
vol.Required(_ATTR_PRESET): vol.All(vol.Coerce(int), vol.Range(min=1)),
})
_SRV_CBW_SCHEMA = CAMERA_SERVICE_SCHEMA.extend({
vol.Required(_ATTR_COLOR_BW): vol.In(_CBW),
})
_SRV_GOTO_SCHEMA = CAMERA_SERVICE_SCHEMA.extend(
{vol.Required(_ATTR_PRESET): vol.All(vol.Coerce(int), vol.Range(min=1))}
)
_SRV_CBW_SCHEMA = CAMERA_SERVICE_SCHEMA.extend(
{vol.Required(_ATTR_COLOR_BW): vol.In(_CBW)}
)
CAMERA_SERVICES = {
_SRV_EN_REC: (CAMERA_SERVICE_SCHEMA, 'async_enable_recording', ()),
_SRV_DS_REC: (CAMERA_SERVICE_SCHEMA, 'async_disable_recording', ()),
_SRV_EN_AUD: (CAMERA_SERVICE_SCHEMA, 'async_enable_audio', ()),
_SRV_DS_AUD: (CAMERA_SERVICE_SCHEMA, 'async_disable_audio', ()),
_SRV_EN_MOT_REC: (
CAMERA_SERVICE_SCHEMA, 'async_enable_motion_recording', ()),
_SRV_DS_MOT_REC: (
CAMERA_SERVICE_SCHEMA, 'async_disable_motion_recording', ()),
_SRV_GOTO: (_SRV_GOTO_SCHEMA, 'async_goto_preset', (_ATTR_PRESET,)),
_SRV_CBW: (_SRV_CBW_SCHEMA, 'async_set_color_bw', (_ATTR_COLOR_BW,)),
_SRV_TOUR_ON: (CAMERA_SERVICE_SCHEMA, 'async_start_tour', ()),
_SRV_TOUR_OFF: (CAMERA_SERVICE_SCHEMA, 'async_stop_tour', ()),
_SRV_EN_REC: (CAMERA_SERVICE_SCHEMA, "async_enable_recording", ()),
_SRV_DS_REC: (CAMERA_SERVICE_SCHEMA, "async_disable_recording", ()),
_SRV_EN_AUD: (CAMERA_SERVICE_SCHEMA, "async_enable_audio", ()),
_SRV_DS_AUD: (CAMERA_SERVICE_SCHEMA, "async_disable_audio", ()),
_SRV_EN_MOT_REC: (CAMERA_SERVICE_SCHEMA, "async_enable_motion_recording", ()),
_SRV_DS_MOT_REC: (CAMERA_SERVICE_SCHEMA, "async_disable_motion_recording", ()),
_SRV_GOTO: (_SRV_GOTO_SCHEMA, "async_goto_preset", (_ATTR_PRESET,)),
_SRV_CBW: (_SRV_CBW_SCHEMA, "async_set_color_bw", (_ATTR_COLOR_BW,)),
_SRV_TOUR_ON: (CAMERA_SERVICE_SCHEMA, "async_start_tour", ()),
_SRV_TOUR_OFF: (CAMERA_SERVICE_SCHEMA, "async_stop_tour", ()),
}
_BOOL_TO_STATE = {True: STATE_ON, False: STATE_OFF}
async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up an Amcrest IP Camera."""
if discovery_info is None:
return
name = discovery_info[CONF_NAME]
device = hass.data[DATA_AMCREST][DEVICES][name]
async_add_entities([
AmcrestCam(name, device, hass.data[DATA_FFMPEG])], True)
async_add_entities([AmcrestCam(name, device, hass.data[DATA_FFMPEG])], True)
class AmcrestCam(Camera):
@ -118,56 +120,59 @@ class AmcrestCam(Camera):
available = self.available
if not available or not self.is_on:
_LOGGER.warning(
'Attempt to take snaphot when %s camera is %s', self.name,
'offline' if not available else 'off')
"Attempt to take snaphot when %s camera is %s",
self.name,
"offline" if not available else "off",
)
return None
async with self._snapshot_lock:
try:
# Send the request to snap a picture and return raw jpg data
response = await self.hass.async_add_executor_job(
self._api.snapshot)
response = await self.hass.async_add_executor_job(self._api.snapshot)
return response.data
except (AmcrestError, HTTPError) as error:
log_update_error(
_LOGGER, 'get image from', self.name, 'camera', error)
log_update_error(_LOGGER, "get image from", self.name, "camera", error)
return None
async def handle_async_mjpeg_stream(self, request):
"""Return an MJPEG stream."""
# The snapshot implementation is handled by the parent class
if self._stream_source == 'snapshot':
if self._stream_source == "snapshot":
return await super().handle_async_mjpeg_stream(request)
if not self.available:
_LOGGER.warning(
'Attempt to stream %s when %s camera is offline',
self._stream_source, self.name)
"Attempt to stream %s when %s camera is offline",
self._stream_source,
self.name,
)
return None
if self._stream_source == 'mjpeg':
if self._stream_source == "mjpeg":
# stream an MJPEG image stream directly from the camera
websession = async_get_clientsession(self.hass)
streaming_url = self._api.mjpeg_url(typeno=self._resolution)
stream_coro = websession.get(
streaming_url, auth=self._token,
timeout=CAMERA_WEB_SESSION_TIMEOUT)
streaming_url, auth=self._token, timeout=CAMERA_WEB_SESSION_TIMEOUT
)
return await async_aiohttp_proxy_web(
self.hass, request, stream_coro)
return await async_aiohttp_proxy_web(self.hass, request, stream_coro)
# streaming via ffmpeg
from haffmpeg.camera import CameraMjpeg
streaming_url = self._rtsp_url
stream = CameraMjpeg(self._ffmpeg.binary, loop=self.hass.loop)
await stream.open_camera(
streaming_url, extra_cmd=self._ffmpeg_arguments)
await stream.open_camera(streaming_url, extra_cmd=self._ffmpeg_arguments)
try:
stream_reader = await stream.get_reader()
return await async_aiohttp_proxy_stream(
self.hass, request, stream_reader,
self._ffmpeg.ffmpeg_stream_content_type)
self.hass,
request,
stream_reader,
self._ffmpeg.ffmpeg_stream_content_type,
)
finally:
await stream.close()
@ -191,10 +196,11 @@ class AmcrestCam(Camera):
"""Return the Amcrest-specific camera state attributes."""
attr = {}
if self._audio_enabled is not None:
attr['audio'] = _BOOL_TO_STATE.get(self._audio_enabled)
attr["audio"] = _BOOL_TO_STATE.get(self._audio_enabled)
if self._motion_recording_enabled is not None:
attr['motion_recording'] = _BOOL_TO_STATE.get(
self._motion_recording_enabled)
attr["motion_recording"] = _BOOL_TO_STATE.get(
self._motion_recording_enabled
)
if self._color_bw is not None:
attr[_ATTR_COLOR_BW] = self._color_bw
return attr
@ -249,13 +255,20 @@ class AmcrestCam(Camera):
async def async_added_to_hass(self):
"""Subscribe to signals and add camera to list."""
for service, params in CAMERA_SERVICES.items():
self._unsub_dispatcher.append(async_dispatcher_connect(
self._unsub_dispatcher.append(
async_dispatcher_connect(
self.hass,
service_signal(service, self.entity_id),
getattr(self, params[1]),
)
)
self._unsub_dispatcher.append(
async_dispatcher_connect(
self.hass,
service_signal(service, self.entity_id),
getattr(self, params[1])))
self._unsub_dispatcher.append(async_dispatcher_connect(
self.hass, service_signal(SERVICE_UPDATE, self._name),
self.async_on_demand_update))
service_signal(SERVICE_UPDATE, self._name),
self.async_on_demand_update,
)
)
self.hass.data[DATA_AMCREST][CAMERAS].append(self.entity_id)
async def async_will_remove_from_hass(self):
@ -270,32 +283,29 @@ class AmcrestCam(Camera):
if not self.available:
self._update_succeeded = False
return
_LOGGER.debug('Updating %s camera', self.name)
_LOGGER.debug("Updating %s camera", self.name)
try:
if self._brand is None:
resp = self._api.vendor_information.strip()
if resp.startswith('vendor='):
self._brand = resp.split('=')[-1]
if resp.startswith("vendor="):
self._brand = resp.split("=")[-1]
else:
self._brand = 'unknown'
self._brand = "unknown"
if self._model is None:
resp = self._api.device_type.strip()
if resp.startswith('type='):
self._model = resp.split('=')[-1]
if resp.startswith("type="):
self._model = resp.split("=")[-1]
else:
self._model = 'unknown'
self._model = "unknown"
self.is_streaming = self._api.video_enabled
self._is_recording = self._api.record_mode == 'Manual'
self._motion_detection_enabled = (
self._api.is_motion_detector_on())
self._is_recording = self._api.record_mode == "Manual"
self._motion_detection_enabled = self._api.is_motion_detector_on()
self._audio_enabled = self._api.audio_enabled
self._motion_recording_enabled = (
self._api.is_record_on_motion_detection())
self._motion_recording_enabled = self._api.is_record_on_motion_detection()
self._color_bw = _CBW[self._api.day_night_color]
self._rtsp_url = self._api.rtsp_url(typeno=self._resolution)
except AmcrestError as error:
log_update_error(
_LOGGER, 'get', self.name, 'camera attributes', error)
log_update_error(_LOGGER, "get", self.name, "camera attributes", error)
self._update_succeeded = False
else:
self._update_succeeded = True
@ -338,13 +348,11 @@ class AmcrestCam(Camera):
async def async_enable_motion_recording(self):
"""Call the job and enable motion recording."""
await self.hass.async_add_executor_job(self._enable_motion_recording,
True)
await self.hass.async_add_executor_job(self._enable_motion_recording, True)
async def async_disable_motion_recording(self):
"""Call the job and disable motion recording."""
await self.hass.async_add_executor_job(self._enable_motion_recording,
False)
await self.hass.async_add_executor_job(self._enable_motion_recording, False)
async def async_goto_preset(self, preset):
"""Call the job and move camera to preset position."""
@ -375,8 +383,12 @@ class AmcrestCam(Camera):
self._api.video_enabled = enable
except AmcrestError as error:
log_update_error(
_LOGGER, 'enable' if enable else 'disable', self.name,
'camera video stream', error)
_LOGGER,
"enable" if enable else "disable",
self.name,
"camera video stream",
error,
)
else:
self.is_streaming = enable
self.schedule_update_ha_state()
@ -390,14 +402,17 @@ class AmcrestCam(Camera):
# video stream off if recording is being turned on.
if not self.is_streaming and enable:
self._enable_video_stream(True)
rec_mode = {'Automatic': 0, 'Manual': 1}
rec_mode = {"Automatic": 0, "Manual": 1}
try:
self._api.record_mode = rec_mode[
'Manual' if enable else 'Automatic']
self._api.record_mode = rec_mode["Manual" if enable else "Automatic"]
except AmcrestError as error:
log_update_error(
_LOGGER, 'enable' if enable else 'disable', self.name,
'camera recording', error)
_LOGGER,
"enable" if enable else "disable",
self.name,
"camera recording",
error,
)
else:
self._is_recording = enable
self.schedule_update_ha_state()
@ -408,8 +423,12 @@ class AmcrestCam(Camera):
self._api.motion_detection = str(enable).lower()
except AmcrestError as error:
log_update_error(
_LOGGER, 'enable' if enable else 'disable', self.name,
'camera motion detection', error)
_LOGGER,
"enable" if enable else "disable",
self.name,
"camera motion detection",
error,
)
else:
self._motion_detection_enabled = enable
self.schedule_update_ha_state()
@ -420,8 +439,12 @@ class AmcrestCam(Camera):
self._api.audio_enabled = enable
except AmcrestError as error:
log_update_error(
_LOGGER, 'enable' if enable else 'disable', self.name,
'camera audio stream', error)
_LOGGER,
"enable" if enable else "disable",
self.name,
"camera audio stream",
error,
)
else:
self._audio_enabled = enable
self.schedule_update_ha_state()
@ -432,12 +455,18 @@ class AmcrestCam(Camera):
"""Enable or disable indicator light."""
try:
self._api.command(
'configManager.cgi?action=setConfig&LightGlobal[0].Enable={}'
.format(str(enable).lower()))
"configManager.cgi?action=setConfig&LightGlobal[0].Enable={}".format(
str(enable).lower()
)
)
except AmcrestError as error:
log_update_error(
_LOGGER, 'enable' if enable else 'disable', self.name,
'indicator light', error)
_LOGGER,
"enable" if enable else "disable",
self.name,
"indicator light",
error,
)
def _enable_motion_recording(self, enable):
"""Enable or disable motion recording."""
@ -445,8 +474,12 @@ class AmcrestCam(Camera):
self._api.motion_recording = str(enable).lower()
except AmcrestError as error:
log_update_error(
_LOGGER, 'enable' if enable else 'disable', self.name,
'camera motion recording', error)
_LOGGER,
"enable" if enable else "disable",
self.name,
"camera motion recording",
error,
)
else:
self._motion_recording_enabled = enable
self.schedule_update_ha_state()
@ -454,12 +487,11 @@ class AmcrestCam(Camera):
def _goto_preset(self, preset):
"""Move camera position and zoom to preset."""
try:
self._api.go_to_preset(
action='start', preset_point_number=preset)
self._api.go_to_preset(action="start", preset_point_number=preset)
except AmcrestError as error:
log_update_error(
_LOGGER, 'move', self.name,
'camera to preset {}'.format(preset), error)
_LOGGER, "move", self.name, "camera to preset {}".format(preset), error
)
def _set_color_bw(self, cbw):
"""Set camera color mode."""
@ -467,8 +499,8 @@ class AmcrestCam(Camera):
self._api.day_night_color = _CBW.index(cbw)
except AmcrestError as error:
log_update_error(
_LOGGER, 'set', self.name,
'camera color mode to {}'.format(cbw), error)
_LOGGER, "set", self.name, "camera color mode to {}".format(cbw), error
)
else:
self._color_bw = cbw
self.schedule_update_ha_state()
@ -479,5 +511,5 @@ class AmcrestCam(Camera):
self._api.tour(start=start)
except AmcrestError as error:
log_update_error(
_LOGGER, 'start' if start else 'stop', self.name,
'camera tour', error)
_LOGGER, "start" if start else "stop", self.name, "camera tour", error
)

View file

@ -1,11 +1,11 @@
"""Constants for amcrest component."""
DOMAIN = 'amcrest'
DOMAIN = "amcrest"
DATA_AMCREST = DOMAIN
CAMERAS = 'cameras'
DEVICES = 'devices'
CAMERAS = "cameras"
DEVICES = "devices"
BINARY_SENSOR_SCAN_INTERVAL_SECS = 5
CAMERA_WEB_SESSION_TIMEOUT = 10
SENSOR_SCAN_INTERVAL_SECS = 10
SERVICE_UPDATE = 'update'
SERVICE_UPDATE = "update"

View file

@ -4,14 +4,18 @@ from .const import DOMAIN
def service_signal(service, ident=None):
"""Encode service and identifier into signal."""
signal = '{}_{}'.format(DOMAIN, service)
signal = "{}_{}".format(DOMAIN, service)
if ident:
signal += '_{}'.format(ident.replace('.', '_'))
signal += "_{}".format(ident.replace(".", "_"))
return signal
def log_update_error(logger, action, name, entity_type, error):
"""Log an update error."""
logger.error(
'Could not %s %s %s due to error: %s',
action, name, entity_type, error.__class__.__name__)
"Could not %s %s %s due to error: %s",
action,
name,
entity_type,
error.__class__.__name__,
)

View file

@ -8,27 +8,25 @@ from homeassistant.const import CONF_NAME, CONF_SENSORS
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity
from .const import (
DATA_AMCREST, DEVICES, SENSOR_SCAN_INTERVAL_SECS, SERVICE_UPDATE)
from .const import DATA_AMCREST, DEVICES, SENSOR_SCAN_INTERVAL_SECS, SERVICE_UPDATE
from .helpers import log_update_error, service_signal
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=SENSOR_SCAN_INTERVAL_SECS)
SENSOR_MOTION_DETECTOR = 'motion_detector'
SENSOR_PTZ_PRESET = 'ptz_preset'
SENSOR_SDCARD = 'sdcard'
SENSOR_MOTION_DETECTOR = "motion_detector"
SENSOR_PTZ_PRESET = "ptz_preset"
SENSOR_SDCARD = "sdcard"
# Sensor types are defined like: Name, units, icon
SENSORS = {
SENSOR_MOTION_DETECTOR: ['Motion Detected', None, 'mdi:run'],
SENSOR_PTZ_PRESET: ['PTZ Preset', None, 'mdi:camera-iris'],
SENSOR_SDCARD: ['SD Used', '%', 'mdi:sd'],
SENSOR_MOTION_DETECTOR: ["Motion Detected", None, "mdi:run"],
SENSOR_PTZ_PRESET: ["PTZ Preset", None, "mdi:camera-iris"],
SENSOR_SDCARD: ["SD Used", "%", "mdi:sd"],
}
async def async_setup_platform(
hass, config, async_add_entities, discovery_info=None):
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up a sensor for an Amcrest IP Camera."""
if discovery_info is None:
return
@ -36,9 +34,12 @@ async def async_setup_platform(
name = discovery_info[CONF_NAME]
device = hass.data[DATA_AMCREST][DEVICES][name]
async_add_entities(
[AmcrestSensor(name, device, sensor_type)
for sensor_type in discovery_info[CONF_SENSORS]],
True)
[
AmcrestSensor(name, device, sensor_type)
for sensor_type in discovery_info[CONF_SENSORS]
],
True,
)
class AmcrestSensor(Entity):
@ -46,7 +47,7 @@ class AmcrestSensor(Entity):
def __init__(self, name, device, sensor_type):
"""Initialize a sensor for Amcrest camera."""
self._name = '{} {}'.format(name, SENSORS[sensor_type][0])
self._name = "{} {}".format(name, SENSORS[sensor_type][0])
self._signal_name = name
self._api = device.api
self._sensor_type = sensor_type
@ -95,7 +96,7 @@ class AmcrestSensor(Entity):
try:
if self._sensor_type == SENSOR_MOTION_DETECTOR:
self._state = self._api.is_motion_detected
self._attrs['Record Mode'] = self._api.record_mode
self._attrs["Record Mode"] = self._api.record_mode
elif self._sensor_type == SENSOR_PTZ_PRESET:
self._state = self._api.ptz_presets_count
@ -103,20 +104,19 @@ class AmcrestSensor(Entity):
elif self._sensor_type == SENSOR_SDCARD:
storage = self._api.storage_all
try:
self._attrs['Total'] = '{:.2f} {}'.format(
*storage['total'])
self._attrs["Total"] = "{:.2f} {}".format(*storage["total"])
except ValueError:
self._attrs['Total'] = '{} {}'.format(*storage['total'])
self._attrs["Total"] = "{} {}".format(*storage["total"])
try:
self._attrs['Used'] = '{:.2f} {}'.format(*storage['used'])
self._attrs["Used"] = "{:.2f} {}".format(*storage["used"])
except ValueError:
self._attrs['Used'] = '{} {}'.format(*storage['used'])
self._attrs["Used"] = "{} {}".format(*storage["used"])
try:
self._state = '{:.2f}'.format(storage['used_percent'])
self._state = "{:.2f}".format(storage["used_percent"])
except ValueError:
self._state = storage['used_percent']
self._state = storage["used_percent"]
except AmcrestError as error:
log_update_error(_LOGGER, 'update', self.name, 'sensor', error)
log_update_error(_LOGGER, "update", self.name, "sensor", error)
async def async_on_demand_update(self):
"""Update state."""
@ -125,8 +125,10 @@ class AmcrestSensor(Entity):
async def async_added_to_hass(self):
"""Subscribe to update signal."""
self._unsub_dispatcher = async_dispatcher_connect(
self.hass, service_signal(SERVICE_UPDATE, self._signal_name),
self.async_on_demand_update)
self.hass,
service_signal(SERVICE_UPDATE, self._signal_name),
self.async_on_demand_update,
)
async def async_will_remove_from_hass(self):
"""Disconnect from update signal."""

View file

@ -12,17 +12,16 @@ from .helpers import log_update_error, service_signal
_LOGGER = logging.getLogger(__name__)
MOTION_DETECTION = 'motion_detection'
MOTION_RECORDING = 'motion_recording'
MOTION_DETECTION = "motion_detection"
MOTION_RECORDING = "motion_recording"
# Switch types are defined like: Name, icon
SWITCHES = {
MOTION_DETECTION: ['Motion Detection', 'mdi:run-fast'],
MOTION_RECORDING: ['Motion Recording', 'mdi:record-rec']
MOTION_DETECTION: ["Motion Detection", "mdi:run-fast"],
MOTION_RECORDING: ["Motion Recording", "mdi:record-rec"],
}
async def async_setup_platform(
hass, config, async_add_entities, discovery_info=None):
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the IP Amcrest camera switch platform."""
if discovery_info is None:
return
@ -30,9 +29,12 @@ async def async_setup_platform(
name = discovery_info[CONF_NAME]
device = hass.data[DATA_AMCREST][DEVICES][name]
async_add_entities(
[AmcrestSwitch(name, device, setting)
for setting in discovery_info[CONF_SWITCHES]],
True)
[
AmcrestSwitch(name, device, setting)
for setting in discovery_info[CONF_SWITCHES]
],
True,
)
class AmcrestSwitch(ToggleEntity):
@ -40,7 +42,7 @@ class AmcrestSwitch(ToggleEntity):
def __init__(self, name, device, setting):
"""Initialize the Amcrest switch."""
self._name = '{} {}'.format(name, SWITCHES[setting][0])
self._name = "{} {}".format(name, SWITCHES[setting][0])
self._signal_name = name
self._api = device.api
self._setting = setting
@ -64,11 +66,11 @@ class AmcrestSwitch(ToggleEntity):
return
try:
if self._setting == MOTION_DETECTION:
self._api.motion_detection = 'true'
self._api.motion_detection = "true"
elif self._setting == MOTION_RECORDING:
self._api.motion_recording = 'true'
self._api.motion_recording = "true"
except AmcrestError as error:
log_update_error(_LOGGER, 'turn on', self.name, 'switch', error)
log_update_error(_LOGGER, "turn on", self.name, "switch", error)
def turn_off(self, **kwargs):
"""Turn setting off."""
@ -76,11 +78,11 @@ class AmcrestSwitch(ToggleEntity):
return
try:
if self._setting == MOTION_DETECTION:
self._api.motion_detection = 'false'
self._api.motion_detection = "false"
elif self._setting == MOTION_RECORDING:
self._api.motion_recording = 'false'
self._api.motion_recording = "false"
except AmcrestError as error:
log_update_error(_LOGGER, 'turn off', self.name, 'switch', error)
log_update_error(_LOGGER, "turn off", self.name, "switch", error)
@property
def available(self):
@ -100,7 +102,7 @@ class AmcrestSwitch(ToggleEntity):
detection = self._api.is_record_on_motion_detection()
self._state = detection
except AmcrestError as error:
log_update_error(_LOGGER, 'update', self.name, 'switch', error)
log_update_error(_LOGGER, "update", self.name, "switch", error)
@property
def icon(self):
@ -114,8 +116,10 @@ class AmcrestSwitch(ToggleEntity):
async def async_added_to_hass(self):
"""Subscribe to update signal."""
self._unsub_dispatcher = async_dispatcher_connect(
self.hass, service_signal(SERVICE_UPDATE, self._signal_name),
self.async_on_demand_update)
self.hass,
service_signal(SERVICE_UPDATE, self._signal_name),
self.async_on_demand_update,
)
async def async_will_remove_from_hass(self):
"""Disconnect from update signal."""

View file

@ -4,8 +4,7 @@ import logging
import voluptuous as vol
from homeassistant.components.air_quality import (
PLATFORM_SCHEMA, AirQualityEntity)
from homeassistant.components.air_quality import PLATFORM_SCHEMA, AirQualityEntity
from homeassistant.const import CONF_NAME
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
@ -13,18 +12,16 @@ from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
ATTRIBUTION = 'Data provided by Ampio'
CONF_STATION_ID = 'station_id'
ATTRIBUTION = "Data provided by Ampio"
CONF_STATION_ID = "station_id"
SCAN_INTERVAL = timedelta(minutes=10)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_STATION_ID): cv.string,
vol.Optional(CONF_NAME): cv.string,
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{vol.Required(CONF_STATION_ID): cv.string, vol.Optional(CONF_NAME): cv.string}
)
async def async_setup_platform(
hass, config, async_add_entities, discovery_info=None):
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the Ampio Smog air quality platform."""
from asmog import AmpioSmog

View file

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

View file

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

View file

@ -2,12 +2,17 @@
from homeassistant.helpers.icon import icon_for_battery_level
from . import (
CONF_HOST, CONF_NAME, CONF_SENSORS, DATA_IP_WEBCAM, ICON_MAP, KEY_MAP,
AndroidIPCamEntity)
CONF_HOST,
CONF_NAME,
CONF_SENSORS,
DATA_IP_WEBCAM,
ICON_MAP,
KEY_MAP,
AndroidIPCamEntity,
)
async def async_setup_platform(
hass, config, async_add_entities, discovery_info=None):
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the IP Webcam Sensor."""
if discovery_info is None:
return
@ -34,7 +39,7 @@ class IPWebcamSensor(AndroidIPCamEntity):
self._sensor = sensor
self._mapped_name = KEY_MAP.get(self._sensor, self._sensor)
self._name = '{} {}'.format(name, self._mapped_name)
self._name = "{} {}".format(name, self._mapped_name)
self._state = None
self._unit = None
@ -55,17 +60,17 @@ class IPWebcamSensor(AndroidIPCamEntity):
async def async_update(self):
"""Retrieve latest state."""
if self._sensor in ('audio_connections', 'video_connections'):
if self._sensor in ("audio_connections", "video_connections"):
if not self._ipcam.status_data:
return
self._state = self._ipcam.status_data.get(self._sensor)
self._unit = 'Connections'
self._unit = "Connections"
else:
self._state, self._unit = self._ipcam.export_sensor(self._sensor)
@property
def icon(self):
"""Return the icon for the sensor."""
if self._sensor == 'battery_level' and self._state is not None:
if self._sensor == "battery_level" and self._state is not None:
return icon_for_battery_level(int(self._state))
return ICON_MAP.get(self._sensor, 'mdi:eye')
return ICON_MAP.get(self._sensor, "mdi:eye")

View file

@ -2,12 +2,17 @@
from homeassistant.components.switch import SwitchDevice
from . import (
CONF_HOST, CONF_NAME, CONF_SWITCHES, DATA_IP_WEBCAM, ICON_MAP, KEY_MAP,
AndroidIPCamEntity)
CONF_HOST,
CONF_NAME,
CONF_SWITCHES,
DATA_IP_WEBCAM,
ICON_MAP,
KEY_MAP,
AndroidIPCamEntity,
)
async def async_setup_platform(
hass, config, async_add_entities, discovery_info=None):
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the IP Webcam switch platform."""
if discovery_info is None:
return
@ -34,7 +39,7 @@ class IPWebcamSettingsSwitch(AndroidIPCamEntity, SwitchDevice):
self._setting = setting
self._mapped_name = KEY_MAP.get(self._setting, self._setting)
self._name = '{} {}'.format(name, self._mapped_name)
self._name = "{} {}".format(name, self._mapped_name)
self._state = False
@property
@ -53,11 +58,11 @@ class IPWebcamSettingsSwitch(AndroidIPCamEntity, SwitchDevice):
async def async_turn_on(self, **kwargs):
"""Turn device on."""
if self._setting == 'torch':
if self._setting == "torch":
await self._ipcam.torch(activate=True)
elif self._setting == 'focus':
elif self._setting == "focus":
await self._ipcam.focus(activate=True)
elif self._setting == 'video_recording':
elif self._setting == "video_recording":
await self._ipcam.record(record=True)
else:
await self._ipcam.change_setting(self._setting, True)
@ -66,11 +71,11 @@ class IPWebcamSettingsSwitch(AndroidIPCamEntity, SwitchDevice):
async def async_turn_off(self, **kwargs):
"""Turn device off."""
if self._setting == 'torch':
if self._setting == "torch":
await self._ipcam.torch(activate=False)
elif self._setting == 'focus':
elif self._setting == "focus":
await self._ipcam.focus(activate=False)
elif self._setting == 'video_recording':
elif self._setting == "video_recording":
await self._ipcam.record(record=False)
else:
await self._ipcam.change_setting(self._setting, False)
@ -80,4 +85,4 @@ class IPWebcamSettingsSwitch(AndroidIPCamEntity, SwitchDevice):
@property
def icon(self):
"""Return the icon for the switch."""
return ICON_MAP.get(self._setting, 'mdi:flash')
return ICON_MAP.get(self._setting, "mdi:flash")

View file

@ -3,81 +3,113 @@ import functools
import logging
import voluptuous as vol
from homeassistant.components.media_player import (
MediaPlayerDevice, PLATFORM_SCHEMA)
from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA
from homeassistant.components.media_player.const import (
SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PREVIOUS_TRACK,
SUPPORT_SELECT_SOURCE, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON,
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP)
SUPPORT_NEXT_TRACK,
SUPPORT_PAUSE,
SUPPORT_PLAY,
SUPPORT_PREVIOUS_TRACK,
SUPPORT_SELECT_SOURCE,
SUPPORT_STOP,
SUPPORT_TURN_OFF,
SUPPORT_TURN_ON,
SUPPORT_VOLUME_MUTE,
SUPPORT_VOLUME_STEP,
)
from homeassistant.const import (
ATTR_COMMAND, ATTR_ENTITY_ID, CONF_DEVICE_CLASS, CONF_HOST, CONF_NAME,
CONF_PORT, STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING,
STATE_STANDBY)
ATTR_COMMAND,
ATTR_ENTITY_ID,
CONF_DEVICE_CLASS,
CONF_HOST,
CONF_NAME,
CONF_PORT,
STATE_IDLE,
STATE_OFF,
STATE_PAUSED,
STATE_PLAYING,
STATE_STANDBY,
)
from homeassistant.exceptions import PlatformNotReady
import homeassistant.helpers.config_validation as cv
ANDROIDTV_DOMAIN = 'androidtv'
ANDROIDTV_DOMAIN = "androidtv"
_LOGGER = logging.getLogger(__name__)
SUPPORT_ANDROIDTV = SUPPORT_PAUSE | SUPPORT_PLAY | \
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PREVIOUS_TRACK | \
SUPPORT_NEXT_TRACK | SUPPORT_STOP | SUPPORT_VOLUME_MUTE | \
SUPPORT_VOLUME_STEP
SUPPORT_ANDROIDTV = (
SUPPORT_PAUSE
| SUPPORT_PLAY
| SUPPORT_TURN_ON
| SUPPORT_TURN_OFF
| SUPPORT_PREVIOUS_TRACK
| SUPPORT_NEXT_TRACK
| SUPPORT_STOP
| SUPPORT_VOLUME_MUTE
| SUPPORT_VOLUME_STEP
)
SUPPORT_FIRETV = SUPPORT_PAUSE | SUPPORT_PLAY | \
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PREVIOUS_TRACK | \
SUPPORT_NEXT_TRACK | SUPPORT_SELECT_SOURCE | SUPPORT_STOP
SUPPORT_FIRETV = (
SUPPORT_PAUSE
| SUPPORT_PLAY
| SUPPORT_TURN_ON
| SUPPORT_TURN_OFF
| SUPPORT_PREVIOUS_TRACK
| SUPPORT_NEXT_TRACK
| SUPPORT_SELECT_SOURCE
| SUPPORT_STOP
)
CONF_ADBKEY = 'adbkey'
CONF_ADB_SERVER_IP = 'adb_server_ip'
CONF_ADB_SERVER_PORT = 'adb_server_port'
CONF_APPS = 'apps'
CONF_GET_SOURCES = 'get_sources'
CONF_TURN_ON_COMMAND = 'turn_on_command'
CONF_TURN_OFF_COMMAND = 'turn_off_command'
CONF_ADBKEY = "adbkey"
CONF_ADB_SERVER_IP = "adb_server_ip"
CONF_ADB_SERVER_PORT = "adb_server_port"
CONF_APPS = "apps"
CONF_GET_SOURCES = "get_sources"
CONF_TURN_ON_COMMAND = "turn_on_command"
CONF_TURN_OFF_COMMAND = "turn_off_command"
DEFAULT_NAME = 'Android TV'
DEFAULT_NAME = "Android TV"
DEFAULT_PORT = 5555
DEFAULT_ADB_SERVER_PORT = 5037
DEFAULT_GET_SOURCES = True
DEFAULT_DEVICE_CLASS = 'auto'
DEFAULT_DEVICE_CLASS = "auto"
DEVICE_ANDROIDTV = 'androidtv'
DEVICE_FIRETV = 'firetv'
DEVICE_ANDROIDTV = "androidtv"
DEVICE_FIRETV = "firetv"
DEVICE_CLASSES = [DEFAULT_DEVICE_CLASS, DEVICE_ANDROIDTV, DEVICE_FIRETV]
SERVICE_ADB_COMMAND = 'adb_command'
SERVICE_ADB_COMMAND = "adb_command"
SERVICE_ADB_COMMAND_SCHEMA = vol.Schema({
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(ATTR_COMMAND): cv.string,
})
SERVICE_ADB_COMMAND_SCHEMA = vol.Schema(
{vol.Required(ATTR_ENTITY_ID): cv.entity_ids, vol.Required(ATTR_COMMAND): cv.string}
)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_DEVICE_CLASS, default=DEFAULT_DEVICE_CLASS):
vol.In(DEVICE_CLASSES),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_ADBKEY): cv.isfile,
vol.Optional(CONF_ADB_SERVER_IP): cv.string,
vol.Optional(CONF_ADB_SERVER_PORT, default=DEFAULT_ADB_SERVER_PORT):
cv.port,
vol.Optional(CONF_GET_SOURCES, default=DEFAULT_GET_SOURCES): cv.boolean,
vol.Optional(CONF_APPS, default=dict()):
vol.Schema({cv.string: cv.string}),
vol.Optional(CONF_TURN_ON_COMMAND): cv.string,
vol.Optional(CONF_TURN_OFF_COMMAND): cv.string
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_DEVICE_CLASS, default=DEFAULT_DEVICE_CLASS): vol.In(
DEVICE_CLASSES
),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_ADBKEY): cv.isfile,
vol.Optional(CONF_ADB_SERVER_IP): cv.string,
vol.Optional(CONF_ADB_SERVER_PORT, default=DEFAULT_ADB_SERVER_PORT): cv.port,
vol.Optional(CONF_GET_SOURCES, default=DEFAULT_GET_SOURCES): cv.boolean,
vol.Optional(CONF_APPS, default=dict()): vol.Schema({cv.string: cv.string}),
vol.Optional(CONF_TURN_ON_COMMAND): cv.string,
vol.Optional(CONF_TURN_OFF_COMMAND): cv.string,
}
)
# Translate from `AndroidTV` / `FireTV` reported state to HA state.
ANDROIDTV_STATES = {'off': STATE_OFF,
'idle': STATE_IDLE,
'standby': STATE_STANDBY,
'playing': STATE_PLAYING,
'paused': STATE_PAUSED}
ANDROIDTV_STATES = {
"off": STATE_OFF,
"idle": STATE_IDLE,
"standby": STATE_STANDBY,
"playing": STATE_PLAYING,
"paused": STATE_PAUSED,
}
def setup_platform(hass, config, add_entities, discovery_info=None):
@ -86,14 +118,15 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
hass.data.setdefault(ANDROIDTV_DOMAIN, {})
host = '{0}:{1}'.format(config[CONF_HOST], config[CONF_PORT])
host = "{0}:{1}".format(config[CONF_HOST], config[CONF_PORT])
if CONF_ADB_SERVER_IP not in config:
# Use "python-adb" (Python ADB implementation)
adb_log = "using Python ADB implementation "
if CONF_ADBKEY in config:
aftv = setup(host, config[CONF_ADBKEY],
device_class=config[CONF_DEVICE_CLASS])
aftv = setup(
host, config[CONF_ADBKEY], device_class=config[CONF_DEVICE_CLASS]
)
adb_log += "with adbkey='{0}'".format(config[CONF_ADBKEY])
else:
@ -101,44 +134,52 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
adb_log += "without adbkey authentication"
else:
# Use "pure-python-adb" (communicate with ADB server)
aftv = setup(host, adb_server_ip=config[CONF_ADB_SERVER_IP],
adb_server_port=config[CONF_ADB_SERVER_PORT],
device_class=config[CONF_DEVICE_CLASS])
aftv = setup(
host,
adb_server_ip=config[CONF_ADB_SERVER_IP],
adb_server_port=config[CONF_ADB_SERVER_PORT],
device_class=config[CONF_DEVICE_CLASS],
)
adb_log = "using ADB server at {0}:{1}".format(
config[CONF_ADB_SERVER_IP], config[CONF_ADB_SERVER_PORT])
config[CONF_ADB_SERVER_IP], config[CONF_ADB_SERVER_PORT]
)
if not aftv.available:
# Determine the name that will be used for the device in the log
if CONF_NAME in config:
device_name = config[CONF_NAME]
elif config[CONF_DEVICE_CLASS] == DEVICE_ANDROIDTV:
device_name = 'Android TV device'
device_name = "Android TV device"
elif config[CONF_DEVICE_CLASS] == DEVICE_FIRETV:
device_name = 'Fire TV device'
device_name = "Fire TV device"
else:
device_name = 'Android TV / Fire TV device'
device_name = "Android TV / Fire TV device"
_LOGGER.warning("Could not connect to %s at %s %s",
device_name, host, adb_log)
_LOGGER.warning("Could not connect to %s at %s %s", device_name, host, adb_log)
raise PlatformNotReady
if host in hass.data[ANDROIDTV_DOMAIN]:
_LOGGER.warning("Platform already setup on %s, skipping", host)
else:
if aftv.DEVICE_CLASS == DEVICE_ANDROIDTV:
device = AndroidTVDevice(aftv, config[CONF_NAME],
config[CONF_APPS],
config.get(CONF_TURN_ON_COMMAND),
config.get(CONF_TURN_OFF_COMMAND))
device_name = config[CONF_NAME] if CONF_NAME in config \
else 'Android TV'
device = AndroidTVDevice(
aftv,
config[CONF_NAME],
config[CONF_APPS],
config.get(CONF_TURN_ON_COMMAND),
config.get(CONF_TURN_OFF_COMMAND),
)
device_name = config[CONF_NAME] if CONF_NAME in config else "Android TV"
else:
device = FireTVDevice(aftv, config[CONF_NAME], config[CONF_APPS],
config[CONF_GET_SOURCES],
config.get(CONF_TURN_ON_COMMAND),
config.get(CONF_TURN_OFF_COMMAND))
device_name = config[CONF_NAME] if CONF_NAME in config \
else 'Fire TV'
device = FireTVDevice(
aftv,
config[CONF_NAME],
config[CONF_APPS],
config[CONF_GET_SOURCES],
config.get(CONF_TURN_ON_COMMAND),
config.get(CONF_TURN_OFF_COMMAND),
)
device_name = config[CONF_NAME] if CONF_NAME in config else "Fire TV"
add_entities([device])
_LOGGER.debug("Setup %s at %s%s", device_name, host, adb_log)
@ -151,26 +192,38 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
"""Dispatch service calls to target entities."""
cmd = service.data.get(ATTR_COMMAND)
entity_id = service.data.get(ATTR_ENTITY_ID)
target_devices = [dev for dev in hass.data[ANDROIDTV_DOMAIN].values()
if dev.entity_id in entity_id]
target_devices = [
dev
for dev in hass.data[ANDROIDTV_DOMAIN].values()
if dev.entity_id in entity_id
]
for target_device in target_devices:
output = target_device.adb_command(cmd)
# log the output, if there is any
if output:
_LOGGER.info("Output of command '%s' from '%s': %s",
cmd, target_device.entity_id, output)
_LOGGER.info(
"Output of command '%s' from '%s': %s",
cmd,
target_device.entity_id,
output,
)
hass.services.register(ANDROIDTV_DOMAIN, SERVICE_ADB_COMMAND,
service_adb_command,
schema=SERVICE_ADB_COMMAND_SCHEMA)
hass.services.register(
ANDROIDTV_DOMAIN,
SERVICE_ADB_COMMAND,
service_adb_command,
schema=SERVICE_ADB_COMMAND_SCHEMA,
)
def adb_decorator(override_available=False):
"""Send an ADB command if the device is available and catch exceptions."""
def _adb_decorator(func):
"""Wait if previous ADB commands haven't finished."""
@functools.wraps(func)
def _adb_exception_catcher(self, *args, **kwargs):
# If the device is unavailable, don't do anything
@ -182,7 +235,9 @@ def adb_decorator(override_available=False):
except self.exceptions as err:
_LOGGER.error(
"Failed to execute an ADB command. ADB connection re-"
"establishing attempt in the next update. Error: %s", err)
"establishing attempt in the next update. Error: %s",
err,
)
self._available = False # pylint: disable=protected-access
return None
@ -194,8 +249,7 @@ def adb_decorator(override_available=False):
class ADBDevice(MediaPlayerDevice):
"""Representation of an Android TV or Fire TV device."""
def __init__(self, aftv, name, apps, turn_on_command,
turn_off_command):
def __init__(self, aftv, name, apps, turn_on_command, turn_off_command):
"""Initialize the Android TV / Fire TV device."""
from androidtv.constants import APPS, KEYS
@ -211,15 +265,23 @@ class ADBDevice(MediaPlayerDevice):
# ADB exceptions to catch
if not self.aftv.adb_server_ip:
# Using "python-adb" (Python ADB implementation)
from adb.adb_protocol import (InvalidChecksumError,
InvalidCommandError,
InvalidResponseError)
from adb.adb_protocol import (
InvalidChecksumError,
InvalidCommandError,
InvalidResponseError,
)
from adb.usb_exceptions import TcpTimeoutException
self.exceptions = (AttributeError, BrokenPipeError, TypeError,
ValueError, InvalidChecksumError,
InvalidCommandError, InvalidResponseError,
TcpTimeoutException)
self.exceptions = (
AttributeError,
BrokenPipeError,
TypeError,
ValueError,
InvalidChecksumError,
InvalidCommandError,
InvalidResponseError,
TcpTimeoutException,
)
else:
# Using "pure-python-adb" (communicate with ADB server)
self.exceptions = (ConnectionResetError, RuntimeError)
@ -248,7 +310,7 @@ class ADBDevice(MediaPlayerDevice):
@property
def device_state_attributes(self):
"""Provide the last ADB command's response as an attribute."""
return {'adb_response': self._adb_response}
return {"adb_response": self._adb_response}
@property
def name(self):
@ -311,12 +373,12 @@ class ADBDevice(MediaPlayerDevice):
"""Send an ADB command to an Android TV / Fire TV device."""
key = self._keys.get(cmd)
if key:
self.aftv.adb_shell('input keyevent {}'.format(key))
self.aftv.adb_shell("input keyevent {}".format(key))
self._adb_response = None
self.schedule_update_ha_state()
return
if cmd == 'GET_PROPERTIES':
if cmd == "GET_PROPERTIES":
self._adb_response = str(self.aftv.get_properties_dict())
self.schedule_update_ha_state()
return self._adb_response
@ -334,16 +396,14 @@ class ADBDevice(MediaPlayerDevice):
class AndroidTVDevice(ADBDevice):
"""Representation of an Android TV device."""
def __init__(self, aftv, name, apps, turn_on_command,
turn_off_command):
def __init__(self, aftv, name, apps, turn_on_command, turn_off_command):
"""Initialize the Android TV device."""
super().__init__(aftv, name, apps, turn_on_command,
turn_off_command)
super().__init__(aftv, name, apps, turn_on_command, turn_off_command)
self._device = None
self._device_properties = self.aftv.device_properties
self._is_volume_muted = None
self._unique_id = self._device_properties.get('serialno')
self._unique_id = self._device_properties.get("serialno")
self._volume_level = None
@adb_decorator(override_available=True)
@ -362,8 +422,9 @@ class AndroidTVDevice(ADBDevice):
return
# Get the updated state and attributes.
state, self._current_app, self._device, self._is_volume_muted, \
self._volume_level = self.aftv.update()
state, self._current_app, self._device, self._is_volume_muted, self._volume_level = (
self.aftv.update()
)
self._state = ANDROIDTV_STATES[state]
@ -416,11 +477,11 @@ class AndroidTVDevice(ADBDevice):
class FireTVDevice(ADBDevice):
"""Representation of a Fire TV device."""
def __init__(self, aftv, name, apps, get_sources,
turn_on_command, turn_off_command):
def __init__(
self, aftv, name, apps, get_sources, turn_on_command, turn_off_command
):
"""Initialize the Fire TV device."""
super().__init__(aftv, name, apps, turn_on_command,
turn_off_command)
super().__init__(aftv, name, apps, turn_on_command, turn_off_command)
self._get_sources = get_sources
self._running_apps = None
@ -441,8 +502,9 @@ class FireTVDevice(ADBDevice):
return
# Get the `state`, `current_app`, and `running_apps`.
state, self._current_app, self._running_apps = \
self.aftv.update(self._get_sources)
state, self._current_app, self._running_apps = self.aftv.update(
self._get_sources
)
self._state = ANDROIDTV_STATES[state]
@ -474,7 +536,7 @@ class FireTVDevice(ADBDevice):
opening it.
"""
if isinstance(source, str):
if not source.startswith('!'):
if not source.startswith("!"):
self.aftv.launch_app(source)
else:
self.aftv.stop_app(source[1:].lstrip())

View file

@ -6,24 +6,26 @@ from datetime import timedelta
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA)
from homeassistant.const import (CONF_HOST, CONF_PASSWORD, CONF_USERNAME)
from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.util import Throttle
_LOGGER = logging.getLogger(__name__)
CONF_PORT_RECV = 'port_recv'
CONF_PORT_SEND = 'port_send'
CONF_PORT_RECV = "port_recv"
CONF_PORT_SEND = "port_send"
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_PORT_RECV): cv.port,
vol.Required(CONF_PORT_SEND): cv.port,
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_HOST): cv.string,
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_PORT_RECV): cv.port,
vol.Required(CONF_PORT_SEND): cv.port,
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_HOST): cv.string,
}
)
def setup_platform(hass, config, add_entities, discovery_info=None):
@ -38,8 +40,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
try:
master = DeviceMaster(
username=username, password=password, read_port=port_send,
write_port=port_recv)
username=username,
password=password,
read_port=port_send,
write_port=port_recv,
)
master.query(ip_addr=host)
except socket.error as ex:
_LOGGER.error("Unable to discover PwrCtrl device: %s", str(ex))
@ -49,8 +54,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
for device in master.devices.values():
parent_device = PwrCtrlDevice(device)
devices.extend(
PwrCtrlSwitch(switch, parent_device)
for switch in device.switches.values()
PwrCtrlSwitch(switch, parent_device) for switch in device.switches.values()
)
add_entities(devices)
@ -72,9 +76,8 @@ class PwrCtrlSwitch(SwitchDevice):
@property
def unique_id(self):
"""Return the unique ID of the device."""
return '{device}-{switch_idx}'.format(
device=self._port.device.host,
switch_idx=self._port.get_index()
return "{device}-{switch_idx}".format(
device=self._port.device.host, switch_idx=self._port.get_index()
)
@property

View file

@ -3,34 +3,48 @@ import logging
import voluptuous as vol
from homeassistant.components.media_player import (
MediaPlayerDevice, PLATFORM_SCHEMA)
from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA
from homeassistant.components.media_player.const import (
SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON,
SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET)
SUPPORT_SELECT_SOURCE,
SUPPORT_TURN_OFF,
SUPPORT_TURN_ON,
SUPPORT_VOLUME_MUTE,
SUPPORT_VOLUME_SET,
)
from homeassistant.const import (
CONF_HOST, CONF_NAME, CONF_PORT, EVENT_HOMEASSISTANT_STOP, STATE_OFF,
STATE_ON)
CONF_HOST,
CONF_NAME,
CONF_PORT,
EVENT_HOMEASSISTANT_STOP,
STATE_OFF,
STATE_ON,
)
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'anthemav'
DOMAIN = "anthemav"
DEFAULT_PORT = 14999
SUPPORT_ANTHEMAV = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE
SUPPORT_ANTHEMAV = (
SUPPORT_VOLUME_SET
| SUPPORT_VOLUME_MUTE
| SUPPORT_TURN_ON
| SUPPORT_TURN_OFF
| SUPPORT_SELECT_SOURCE
)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
}
)
async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up our socket to the AVR."""
import anthemav
@ -47,8 +61,8 @@ async def async_setup_platform(hass, config, async_add_entities,
hass.async_create_task(device.async_update_ha_state())
avr = await anthemav.Connection.create(
host=host, port=port,
update_callback=async_anthemav_update_callback)
host=host, port=port, update_callback=async_anthemav_update_callback
)
device = AnthemAVR(avr, name)
@ -84,12 +98,12 @@ class AnthemAVR(MediaPlayerDevice):
@property
def name(self):
"""Return name of device."""
return self._name or self._lookup('model')
return self._name or self._lookup("model")
@property
def state(self):
"""Return state of power on/off."""
pwrstate = self._lookup('power')
pwrstate = self._lookup("power")
if pwrstate is True:
return STATE_ON
@ -100,64 +114,64 @@ class AnthemAVR(MediaPlayerDevice):
@property
def is_volume_muted(self):
"""Return boolean reflecting mute state on device."""
return self._lookup('mute', False)
return self._lookup("mute", False)
@property
def volume_level(self):
"""Return volume level from 0 to 1."""
return self._lookup('volume_as_percentage', 0.0)
return self._lookup("volume_as_percentage", 0.0)
@property
def media_title(self):
"""Return current input name (closest we have to media title)."""
return self._lookup('input_name', 'No Source')
return self._lookup("input_name", "No Source")
@property
def app_name(self):
"""Return details about current video and audio stream."""
return self._lookup('video_input_resolution_text', '') + ' ' \
+ self._lookup('audio_input_name', '')
return (
self._lookup("video_input_resolution_text", "")
+ " "
+ self._lookup("audio_input_name", "")
)
@property
def source(self):
"""Return currently selected input."""
return self._lookup('input_name', "Unknown")
return self._lookup("input_name", "Unknown")
@property
def source_list(self):
"""Return all active, configured inputs."""
return self._lookup('input_list', ["Unknown"])
return self._lookup("input_list", ["Unknown"])
async def async_select_source(self, source):
"""Change AVR to the designated source (by name)."""
self._update_avr('input_name', source)
self._update_avr("input_name", source)
async def async_turn_off(self):
"""Turn AVR power off."""
self._update_avr('power', False)
self._update_avr("power", False)
async def async_turn_on(self):
"""Turn AVR power on."""
self._update_avr('power', True)
self._update_avr("power", True)
async def async_set_volume_level(self, volume):
"""Set AVR volume (0 to 1)."""
self._update_avr('volume_as_percentage', volume)
self._update_avr("volume_as_percentage", volume)
async def async_mute_volume(self, mute):
"""Engage AVR mute."""
self._update_avr('mute', mute)
self._update_avr("mute", mute)
def _update_avr(self, propname, value):
"""Update a property in the AVR."""
_LOGGER.info(
"Sending command to AVR: set %s to %s", propname, str(value))
_LOGGER.info("Sending command to AVR: set %s to %s", propname, str(value))
setattr(self.avr.protocol, propname, value)
@property
def dump_avrdata(self):
"""Return state of avr object for debugging forensics."""
attrs = vars(self)
return(
'dump_avrdata: '
+ ', '.join('%s: %s' % item for item in attrs.items()))
return "dump_avrdata: " + ", ".join("%s: %s" % item for item in attrs.items())

View file

@ -7,26 +7,36 @@ from aiokafka import AIOKafkaProducer
import voluptuous as vol
from homeassistant.const import (
CONF_IP_ADDRESS, CONF_PORT, EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED,
STATE_UNAVAILABLE, STATE_UNKNOWN)
CONF_IP_ADDRESS,
CONF_PORT,
EVENT_HOMEASSISTANT_STOP,
EVENT_STATE_CHANGED,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entityfilter import FILTER_SCHEMA
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'apache_kafka'
DOMAIN = "apache_kafka"
CONF_FILTER = 'filter'
CONF_TOPIC = 'topic'
CONF_FILTER = "filter"
CONF_TOPIC = "topic"
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_IP_ADDRESS): cv.string,
vol.Required(CONF_PORT): cv.port,
vol.Required(CONF_TOPIC): cv.string,
vol.Optional(CONF_FILTER, default={}): FILTER_SCHEMA,
}),
}, extra=vol.ALLOW_EXTRA)
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{
vol.Required(CONF_IP_ADDRESS): cv.string,
vol.Required(CONF_PORT): cv.port,
vol.Required(CONF_TOPIC): cv.string,
vol.Optional(CONF_FILTER, default={}): FILTER_SCHEMA,
}
)
},
extra=vol.ALLOW_EXTRA,
)
async def async_setup(hass, config):
@ -38,7 +48,8 @@ async def async_setup(hass, config):
conf[CONF_IP_ADDRESS],
conf[CONF_PORT],
conf[CONF_TOPIC],
conf[CONF_FILTER])
conf[CONF_FILTER],
)
hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, kafka.shutdown())
@ -63,13 +74,7 @@ class DateTimeJSONEncoder(json.JSONEncoder):
class KafkaManager:
"""Define a manager to buffer events to Kafka."""
def __init__(
self,
hass,
ip_address,
port,
topic,
entities_filter):
def __init__(self, hass, ip_address, port, topic, entities_filter):
"""Initialize."""
self._encoder = DateTimeJSONEncoder()
self._entities_filter = entities_filter
@ -83,16 +88,17 @@ class KafkaManager:
def _encode_event(self, event):
"""Translate events into a binary JSON payload."""
state = event.data.get('new_state')
if (state is None
or state.state in (STATE_UNKNOWN, '', STATE_UNAVAILABLE)
or not self._entities_filter(state.entity_id)):
state = event.data.get("new_state")
if (
state is None
or state.state in (STATE_UNKNOWN, "", STATE_UNAVAILABLE)
or not self._entities_filter(state.entity_id)
):
return
return json.dumps(
obj=state.as_dict(),
default=self._encoder.encode
).encode('utf-8')
return json.dumps(obj=state.as_dict(), default=self._encoder.encode).encode(
"utf-8"
)
async def start(self):
"""Start the Kafka manager."""

View file

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

View file

@ -1,16 +1,15 @@
"""Support for tracking the online status of a UPS."""
import voluptuous as vol
from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA)
from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA
from homeassistant.const import CONF_NAME
import homeassistant.helpers.config_validation as cv
from homeassistant.components import apcupsd
DEFAULT_NAME = 'UPS Online Status'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
})
DEFAULT_NAME = "UPS Online Status"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string}
)
def setup_platform(hass, config, add_entities, discovery_info=None):

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