Merge pull request #16940 from home-assistant/rc

0.79.0
This commit is contained in:
Paulus Schoutsen 2018-09-28 16:51:25 +02:00 committed by GitHub
commit c93879074b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
427 changed files with 10612 additions and 2871 deletions

View File

@ -42,6 +42,7 @@ omit =
homeassistant/components/asterisk_mbox.py
homeassistant/components/*/asterisk_mbox.py
homeassistant/components/*/asterisk_cdr.py
homeassistant/components/august.py
homeassistant/components/*/august.py
@ -92,6 +93,9 @@ omit =
homeassistant/components/ecobee.py
homeassistant/components/*/ecobee.py
homeassistant/components/edp_redy.py
homeassistant/components/*/edp_redy.py
homeassistant/components/egardia.py
homeassistant/components/*/egardia.py
@ -123,6 +127,7 @@ omit =
homeassistant/components/hangouts/const.py
homeassistant/components/hangouts/hangouts_bot.py
homeassistant/components/hangouts/hangups_utils.py
homeassistant/components/hangouts/intents.py
homeassistant/components/*/hangouts.py
homeassistant/components/hdmi_cec.py
@ -140,6 +145,9 @@ omit =
homeassistant/components/homematicip_cloud.py
homeassistant/components/*/homematicip_cloud.py
homeassistant/components/huawei_lte.py
homeassistant/components/*/huawei_lte.py
homeassistant/components/hydrawise.py
homeassistant/components/*/hydrawise.py
@ -183,6 +191,9 @@ omit =
homeassistant/components/linode.py
homeassistant/components/*/linode.py
homeassistant/components/logi_circle.py
homeassistant/components/*/logi_circle.py
homeassistant/components/lutron.py
homeassistant/components/*/lutron.py
@ -684,6 +695,7 @@ omit =
homeassistant/components/sensor/kwb.py
homeassistant/components/sensor/lacrosse.py
homeassistant/components/sensor/lastfm.py
homeassistant/components/sensor/linky.py
homeassistant/components/sensor/linux_battery.py
homeassistant/components/sensor/loopenergy.py
homeassistant/components/sensor/luftdaten.py
@ -740,6 +752,7 @@ omit =
homeassistant/components/sensor/sonarr.py
homeassistant/components/sensor/speedtest.py
homeassistant/components/sensor/spotcrime.py
homeassistant/components/sensor/starlingbank.py
homeassistant/components/sensor/steam_online.py
homeassistant/components/sensor/supervisord.py
homeassistant/components/sensor/swiss_hydrological_data.py
@ -813,6 +826,7 @@ omit =
homeassistant/components/weather/bom.py
homeassistant/components/weather/buienradar.py
homeassistant/components/weather/darksky.py
homeassistant/components/weather/met.py
homeassistant/components/weather/metoffice.py
homeassistant/components/weather/openweathermap.py
homeassistant/components/weather/zamg.py

View File

@ -72,6 +72,7 @@ homeassistant/components/sensor/airvisual.py @bachya
homeassistant/components/sensor/filter.py @dgomes
homeassistant/components/sensor/gearbest.py @HerrHofrat
homeassistant/components/sensor/irish_rail_transport.py @ttroy50
homeassistant/components/sensor/jewish_calendar.py @tsvi
homeassistant/components/sensor/miflora.py @danielhiversen @ChristianKuehnel
homeassistant/components/sensor/nsw_fuel_station.py @nickw444
homeassistant/components/sensor/pollen.py @bachya
@ -97,6 +98,8 @@ homeassistant/components/*/eight_sleep.py @mezz64
homeassistant/components/hive.py @Rendili @KJonline
homeassistant/components/*/hive.py @Rendili @KJonline
homeassistant/components/homekit/* @cdce8p
homeassistant/components/huawei_lte.py @scop
homeassistant/components/*/huawei_lte.py @scop
homeassistant/components/knx.py @Julius2342
homeassistant/components/*/knx.py @Julius2342
homeassistant/components/konnected.py @heythisisnate
@ -117,9 +120,13 @@ homeassistant/components/*/tesla.py @zabuldon
homeassistant/components/tellduslive.py @molobrakos @fredrike
homeassistant/components/*/tellduslive.py @molobrakos @fredrike
homeassistant/components/*/tradfri.py @ggravlingen
homeassistant/components/upcloud.py @scop
homeassistant/components/*/upcloud.py @scop
homeassistant/components/velux.py @Julius2342
homeassistant/components/*/velux.py @Julius2342
homeassistant/components/*/xiaomi_aqara.py @danielhiversen @syssi
homeassistant/components/*/xiaomi_miio.py @rytilahti @syssi
homeassistant/components/zoneminder.py @rohankapoorcom
homeassistant/components/*/zoneminder.py @rohankapoorcom
homeassistant/scripts/check_config.py @kellerza

View File

@ -10,5 +10,5 @@ The process is straight-forward.
- Ensure tests work.
- Create a Pull Request against the [**dev**](https://github.com/home-assistant/home-assistant/tree/dev) branch of Home Assistant.
Still interested? Then you should take a peek at the [developer documentation](https://home-assistant.io/developers/) to get more details.
Still interested? Then you should take a peek at the [developer documentation](https://developers.home-assistant.io/) to get more details.

View File

@ -21,8 +21,8 @@ Featured integrations
|screenshot-components|
The system is built using a modular approach so support for other devices or actions can be implemented easily. See also the `section on architecture <https://home-assistant.io/developers/architecture/>`__ and the `section on creating your own
components <https://home-assistant.io/developers/creating_components/>`__.
The system is built using a modular approach so support for other devices or actions can be implemented easily. See also the `section on architecture <https://developers.home-assistant.io/docs/en/architecture_index.html>`__ and the `section on creating your own
components <https://developers.home-assistant.io/docs/en/creating_component_index.html>`__.
If you run into issues while using Home Assistant or during development
of a component, check the `Home Assistant help section <https://home-assistant.io/help/>`__ of our website for further help and information.

View File

@ -19,4 +19,4 @@ Indices and tables
* :ref:`modindex`
* :ref:`search`
.. _Home Assistant developers: https://home-assistant.io/developers/
.. _Home Assistant developers: https://developers.home-assistant.io/

View File

@ -7,7 +7,6 @@ import platform
import subprocess
import sys
import threading
from typing import List, Dict, Any # noqa pylint: disable=unused-import
@ -20,15 +19,19 @@ from homeassistant.const import (
)
def attempt_use_uvloop() -> None:
def set_loop() -> None:
"""Attempt to use uvloop."""
import asyncio
try:
import uvloop
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
except ImportError:
pass
if sys.platform == 'win32':
asyncio.set_event_loop(asyncio.ProactorEventLoop())
else:
try:
import uvloop
except ImportError:
pass
else:
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
def validate_python() -> None:
@ -240,51 +243,39 @@ def cmdline() -> List[str]:
return [arg for arg in sys.argv if arg != '--daemon']
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."""
from homeassistant import bootstrap
from homeassistant import bootstrap, core
# 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']
while True:
try:
subprocess.check_call(nt_args)
sys.exit(0)
except subprocess.CalledProcessError as exc:
if exc.returncode != RESTART_EXIT_CODE:
sys.exit(exc.returncode)
hass = core.HomeAssistant()
if args.demo_mode:
config = {
'frontend': {},
'demo': {}
} # type: Dict[str, Any]
hass = bootstrap.from_config_dict(
config, config_dir=config_dir, verbose=args.verbose,
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)
else:
config_file = ensure_config_file(config_dir)
print('Config directory:', config_dir)
hass = bootstrap.from_config_file(
config_file, verbose=args.verbose, skip_pip=args.skip_pip,
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)
if hass is None:
return -1
if args.open_ui:
# Imported here to avoid importing asyncio before monkey patch
from homeassistant.util.async_ import run_callback_threadsafe
def open_browser(_: Any) -> None:
"""Open the web interface in a browser."""
if hass.config.api is not None: # type: ignore
if hass.config.api is not None:
import webbrowser
webbrowser.open(hass.config.api.base_url) # type: ignore
webbrowser.open(hass.config.api.base_url)
run_callback_threadsafe(
hass.loop,
@ -292,7 +283,7 @@ def setup_and_run_hass(config_dir: str,
EVENT_HOMEASSISTANT_START, open_browser
)
return hass.start()
return await hass.async_run()
def try_to_restart() -> None:
@ -347,7 +338,20 @@ def main() -> int:
monkey_patch.disable_c_asyncio()
monkey_patch.patch_weakref_tasks()
attempt_use_uvloop()
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']
while True:
try:
subprocess.check_call(nt_args)
sys.exit(0)
except KeyboardInterrupt:
sys.exit(0)
except subprocess.CalledProcessError as exc:
if exc.returncode != RESTART_EXIT_CODE:
sys.exit(exc.returncode)
args = get_arguments()
@ -366,11 +370,12 @@ def main() -> int:
if args.pid_file:
write_pid(args.pid_file)
exit_code = setup_and_run_hass(config_dir, args)
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()
return exit_code
return exit_code # type: ignore # mypy cannot yet infer it
if __name__ == "__main__":

View File

@ -2,3 +2,4 @@
from datetime import timedelta
ACCESS_TOKEN_EXPIRATION = timedelta(minutes=30)
MFA_SESSION_EXPIRATION = timedelta(minutes=5)

View File

@ -1,5 +1,4 @@
"""Plugable auth modules for Home Assistant."""
from datetime import timedelta
import importlib
import logging
import types
@ -23,8 +22,6 @@ MULTI_FACTOR_AUTH_MODULE_SCHEMA = vol.Schema({
vol.Optional(CONF_ID): str,
}, extra=vol.ALLOW_EXTRA)
SESSION_EXPIRATION = timedelta(minutes=5)
DATA_REQS = 'mfa_auth_module_reqs_processed'
_LOGGER = logging.getLogger(__name__)
@ -34,6 +31,7 @@ class MultiFactorAuthModule:
"""Multi-factor Auth Module of validation function."""
DEFAULT_TITLE = 'Unnamed auth module'
MAX_RETRY_TIME = 3
def __init__(self, hass: HomeAssistant, config: Dict[str, Any]) -> None:
"""Initialize an auth module."""
@ -84,7 +82,7 @@ class MultiFactorAuthModule:
"""Return whether user is setup."""
raise NotImplementedError
async def async_validation(
async def async_validate(
self, user_id: str, user_input: Dict[str, Any]) -> bool:
"""Return True if validation passed."""
raise NotImplementedError

View File

@ -77,7 +77,7 @@ class InsecureExampleModule(MultiFactorAuthModule):
return True
return False
async def async_validation(
async def async_validate(
self, user_id: str, user_input: Dict[str, Any]) -> bool:
"""Return True if validation passed."""
for data in self._data:

View File

@ -0,0 +1,325 @@
"""HMAC-based One-time Password auth module.
Sending HOTP through notify service
"""
import logging
from collections import OrderedDict
from typing import Any, Dict, Optional, Tuple, List # noqa: F401
import attr
import voluptuous as vol
from homeassistant.const import CONF_EXCLUDE, CONF_INCLUDE
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv
from . import MultiFactorAuthModule, MULTI_FACTOR_AUTH_MODULES, \
MULTI_FACTOR_AUTH_MODULE_SCHEMA, SetupFlow
REQUIREMENTS = ['pyotp==2.2.6']
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)
STORAGE_VERSION = 1
STORAGE_KEY = 'auth_module.notify'
STORAGE_USERS = 'users'
STORAGE_USER_ID = 'user_id'
INPUT_FIELD_CODE = 'code'
_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')))
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))
@attr.s(slots=True)
class NotifySetting:
"""Store notify setting for one user."""
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)
_UsersDict = Dict[str, NotifySetting]
@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'
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)
self._include = config.get(CONF_INCLUDE, [])
self._exclude = config.get(CONF_EXCLUDE, [])
self._message_template = config[CONF_MESSAGE]
@property
def input_schema(self) -> vol.Schema:
"""Validate login flow input data."""
return vol.Schema({INPUT_FIELD_CODE: str})
async def _async_load(self) -> None:
"""Load stored data."""
data = await self._user_store.async_load()
if data is None:
data = {STORAGE_USERS: {}}
self._user_settings = {
user_id: NotifySetting(**setting)
for user_id, setting in data.get(STORAGE_USERS, {}).items()
}
async def _async_save(self) -> None:
"""Save data."""
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()
}})
@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', {}):
if service not in self._exclude:
unordered_services.add(service)
if self._include:
unordered_services &= set(self._include)
return sorted(unordered_services)
async def async_setup_flow(self, user_id: str) -> SetupFlow:
"""Return a data entry flow handler for setup module.
Mfa module should extend SetupFlow
"""
return NotifySetupFlow(
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."""
if self._user_settings is None:
await self._async_load()
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'),
)
await self._async_save()
async def async_depose_user(self, user_id: str) -> None:
"""Depose auth module for user."""
if self._user_settings is None:
await self._async_load()
assert self._user_settings is not None
if self._user_settings.pop(user_id, None):
await self._async_save()
async def async_is_user_setup(self, user_id: str) -> bool:
"""Return whether user is setup."""
if self._user_settings is None:
await self._async_load()
assert self._user_settings is not None
return user_id in self._user_settings
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()
assert self._user_settings is not None
notify_setting = self._user_settings.get(user_id, None)
if notify_setting is None:
return False
# 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)
async def async_initialize_login_mfa_step(self, user_id: str) -> None:
"""Generate code and notify user."""
if self._user_settings is None:
await self._async_load()
assert self._user_settings is not None
notify_setting = self._user_settings.get(user_id, None)
if notify_setting is None:
raise ValueError('Cannot find user_id')
def generate_secret_and_one_time_password() -> str:
"""Generate and send one time password."""
assert notify_setting
# 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)
code = await self.hass.async_add_executor_job(
generate_secret_and_one_time_password)
await self.async_notify_user(user_id, code)
async def async_notify_user(self, user_id: str, code: str) -> None:
"""Send code by user's notify service."""
if self._user_settings is None:
await self._async_load()
assert self._user_settings is not None
notify_setting = self._user_settings.get(user_id, None)
if notify_setting is None:
_LOGGER.error('Cannot find user %s', user_id)
return
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:
"""Send code by notify service."""
data = {'message': self._message_template.format(code)}
if target:
data['target'] = [target]
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:
"""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._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]:
"""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._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')
schema = OrderedDict() # type: Dict[str, Any]
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
)
async def async_step_setup(
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)
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={}
)
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)
assert self._notify_service
await self._auth_module.async_notify(
code, self._notify_service, self._target)
return self.async_show_form(
step_id='setup',
data_schema=self._setup_schema,
description_placeholders={'notify_service': self._notify_service},
errors=errors,
)

View File

@ -60,6 +60,7 @@ class TotpAuthModule(MultiFactorAuthModule):
"""Auth module validate 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:
"""Initialize the user data store."""
@ -130,7 +131,7 @@ class TotpAuthModule(MultiFactorAuthModule):
return user_id in self._users # type: ignore
async def async_validation(
async def async_validate(
self, user_id: str, user_input: Dict[str, Any]) -> bool:
"""Return True if validation passed."""
if self._users is None:
@ -149,10 +150,10 @@ class TotpAuthModule(MultiFactorAuthModule):
if ota_secret is None:
# even we cannot find user, we still do verify
# to make timing the same as if user was found.
pyotp.TOTP(DUMMY_SECRET).verify(code)
pyotp.TOTP(DUMMY_SECRET).verify(code, valid_window=1)
return False
return bool(pyotp.TOTP(ota_secret).verify(code))
return bool(pyotp.TOTP(ota_secret).verify(code, valid_window=1))
class TotpSetupFlow(SetupFlow):

View File

@ -15,8 +15,8 @@ from homeassistant.util import dt as dt_util
from homeassistant.util.decorator import Registry
from ..auth_store import AuthStore
from ..const import MFA_SESSION_EXPIRATION
from ..models import Credentials, User, UserMeta # noqa: F401
from ..mfa_modules import SESSION_EXPIRATION
_LOGGER = logging.getLogger(__name__)
DATA_REQS = 'auth_prov_reqs_processed'
@ -171,6 +171,7 @@ class LoginFlow(data_entry_flow.FlowHandler):
self._auth_manager = auth_provider.hass.auth # type: ignore
self.available_mfa_modules = {} # type: Dict[str, str]
self.created_at = dt_util.utcnow()
self.invalid_mfa_times = 0
self.user = None # type: Optional[User]
async def async_step_init(
@ -212,6 +213,8 @@ class LoginFlow(data_entry_flow.FlowHandler):
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(
@ -221,25 +224,34 @@ class LoginFlow(data_entry_flow.FlowHandler):
# 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'):
await auth_module.async_initialize_login_mfa_step(self.user.id)
if user_input is not None:
expires = self.created_at + SESSION_EXPIRATION
expires = self.created_at + MFA_SESSION_EXPIRATION
if dt_util.utcnow() > expires:
return self.async_abort(
reason='login_expired'
)
result = await auth_module.async_validation(
self.user.id, user_input) # type: ignore
result = await auth_module.async_validate(
self.user.id, user_input)
if not result:
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'
)
if not errors:
return await self.async_finish(self.user)
description_placeholders = {
'mfa_module_name': auth_module.name,
'mfa_module_id': auth_module.id
} # type: Dict[str, str]
'mfa_module_id': auth_module.id,
} # type: Dict[str, Optional[str]]
return self.async_show_form(
step_id='mfa',

View File

@ -5,7 +5,6 @@ import os
import sys
from time import time
from collections import OrderedDict
from typing import Any, Optional, Dict
import voluptuous as vol
@ -19,7 +18,6 @@ from homeassistant.util.logging import AsyncHandler
from homeassistant.util.package import async_get_user_site, is_virtual_env
from homeassistant.util.yaml import clear_secret_cache
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.signal import async_register_signal_handling
_LOGGER = logging.getLogger(__name__)
@ -160,7 +158,6 @@ async def async_from_config_dict(config: Dict[str, Any],
stop = time()
_LOGGER.info("Home Assistant initialized in %.2fs", stop-start)
async_register_signal_handling(hass)
return hass

View File

@ -4,71 +4,65 @@ Support for Vanderbilt (formerly Siemens) SPC alarm systems.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/alarm_control_panel.spc/
"""
import asyncio
import logging
import homeassistant.components.alarm_control_panel as alarm
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.core import callback
from homeassistant.components.spc import (
ATTR_DISCOVER_AREAS, DATA_API, DATA_REGISTRY, SpcWebGateway)
ATTR_DISCOVER_AREAS, DATA_API, SIGNAL_UPDATE_ALARM)
from homeassistant.const import (
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
STATE_UNKNOWN)
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT,
STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED)
_LOGGER = logging.getLogger(__name__)
SPC_AREA_MODE_TO_STATE = {
'0': STATE_ALARM_DISARMED,
'1': STATE_ALARM_ARMED_HOME,
'3': STATE_ALARM_ARMED_AWAY,
}
def _get_alarm_state(spc_mode):
def _get_alarm_state(area):
"""Get the alarm state."""
return SPC_AREA_MODE_TO_STATE.get(spc_mode, STATE_UNKNOWN)
from pyspcwebgw.const import AreaMode
if area.verified_alarm:
return STATE_ALARM_TRIGGERED
mode_to_state = {
AreaMode.UNSET: STATE_ALARM_DISARMED,
AreaMode.PART_SET_A: STATE_ALARM_ARMED_HOME,
AreaMode.PART_SET_B: STATE_ALARM_ARMED_NIGHT,
AreaMode.FULL_SET: STATE_ALARM_ARMED_AWAY,
}
return mode_to_state.get(area.mode)
@asyncio.coroutine
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 SPC alarm control panel platform."""
if (discovery_info is None or
discovery_info[ATTR_DISCOVER_AREAS] is None):
return
api = hass.data[DATA_API]
devices = [SpcAlarm(api, area)
for area in discovery_info[ATTR_DISCOVER_AREAS]]
async_add_entities(devices)
async_add_entities([SpcAlarm(area=area, api=hass.data[DATA_API])
for area in discovery_info[ATTR_DISCOVER_AREAS]])
class SpcAlarm(alarm.AlarmControlPanel):
"""Representation of the SPC alarm panel."""
def __init__(self, api, area):
def __init__(self, area, api):
"""Initialize the SPC alarm panel."""
self._area_id = area['id']
self._name = area['name']
self._state = _get_alarm_state(area['mode'])
if self._state == STATE_ALARM_DISARMED:
self._changed_by = area.get('last_unset_user_name', 'unknown')
else:
self._changed_by = area.get('last_set_user_name', 'unknown')
self._area = area
self._api = api
@asyncio.coroutine
def async_added_to_hass(self):
async def async_added_to_hass(self):
"""Call for adding new entities."""
self.hass.data[DATA_REGISTRY].register_alarm_device(
self._area_id, self)
async_dispatcher_connect(self.hass,
SIGNAL_UPDATE_ALARM.format(self._area.id),
self._update_callback)
@asyncio.coroutine
def async_update_from_spc(self, state, extra):
"""Update the alarm panel with a new state."""
self._state = state
self._changed_by = extra.get('changed_by', 'unknown')
self.async_schedule_update_ha_state()
@callback
def _update_callback(self):
"""Call update method."""
self.async_schedule_update_ha_state(True)
@property
def should_poll(self):
@ -78,32 +72,34 @@ class SpcAlarm(alarm.AlarmControlPanel):
@property
def name(self):
"""Return the name of the device."""
return self._name
return self._area.name
@property
def changed_by(self):
"""Return the user the last change was triggered by."""
return self._changed_by
return self._area.last_changed_by
@property
def state(self):
"""Return the state of the device."""
return self._state
return _get_alarm_state(self._area)
@asyncio.coroutine
def async_alarm_disarm(self, code=None):
async def async_alarm_disarm(self, code=None):
"""Send disarm command."""
yield from self._api.send_area_command(
self._area_id, SpcWebGateway.AREA_COMMAND_UNSET)
from pyspcwebgw.const import AreaMode
self._api.change_mode(area=self._area, new_mode=AreaMode.UNSET)
@asyncio.coroutine
def async_alarm_arm_home(self, code=None):
async def async_alarm_arm_home(self, code=None):
"""Send arm home command."""
yield from self._api.send_area_command(
self._area_id, SpcWebGateway.AREA_COMMAND_PART_SET)
from pyspcwebgw.const import AreaMode
self._api.change_mode(area=self._area, new_mode=AreaMode.PART_SET_A)
@asyncio.coroutine
def async_alarm_arm_away(self, code=None):
async def async_alarm_arm_night(self, code=None):
"""Send arm home command."""
from pyspcwebgw.const import AreaMode
self._api.change_mode(area=self._area, new_mode=AreaMode.PART_SET_B)
async def async_alarm_arm_away(self, code=None):
"""Send arm away command."""
yield from self._api.send_area_command(
self._area_id, SpcWebGateway.AREA_COMMAND_SET)
from pyspcwebgw.const import AreaMode
self._api.change_mode(area=self._area, new_mode=AreaMode.FULL_SET)

View File

@ -1529,3 +1529,8 @@ async def async_api_reportstate(hass, config, request, context, entity):
name='StateReport',
context={'properties': properties}
)
def turned_off_response(message):
"""Return a device turned off response."""
return api_error(message[API_DIRECTIVE], error_type='BRIDGE_UNREACHABLE')

View File

@ -6,7 +6,6 @@ https://home-assistant.io/components/apple_tv/
"""
import asyncio
import logging
from typing import Sequence, TypeVar, Union
import voluptuous as vol

View File

@ -13,7 +13,7 @@ from homeassistant.core import callback
from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect, async_dispatcher_send)
async_dispatcher_send, dispatcher_connect)
REQUIREMENTS = ['asterisk_mbox==0.5.0']
@ -21,8 +21,11 @@ _LOGGER = logging.getLogger(__name__)
DOMAIN = 'asterisk_mbox'
SIGNAL_DISCOVER_PLATFORM = "asterisk_mbox.discover_platform"
SIGNAL_MESSAGE_REQUEST = 'asterisk_mbox.message_request'
SIGNAL_MESSAGE_UPDATE = 'asterisk_mbox.message_updated'
SIGNAL_CDR_UPDATE = 'asterisk_mbox.message_updated'
SIGNAL_CDR_REQUEST = 'asterisk_mbox.message_request'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
@ -41,9 +44,7 @@ def setup(hass, config):
port = conf.get(CONF_PORT)
password = conf.get(CONF_PASSWORD)
hass.data[DOMAIN] = AsteriskData(hass, host, port, password)
discovery.load_platform(hass, 'mailbox', DOMAIN, {}, config)
hass.data[DOMAIN] = AsteriskData(hass, host, port, password, config)
return True
@ -51,31 +52,71 @@ def setup(hass, config):
class AsteriskData:
"""Store Asterisk mailbox data."""
def __init__(self, hass, host, port, password):
def __init__(self, hass, host, port, password, config):
"""Init the Asterisk data object."""
from asterisk_mbox import Client as asteriskClient
self.hass = hass
self.client = asteriskClient(host, port, password, self.handle_data)
self.messages = []
self.config = config
self.messages = None
self.cdr = None
async_dispatcher_connect(
dispatcher_connect(
self.hass, SIGNAL_MESSAGE_REQUEST, self._request_messages)
dispatcher_connect(
self.hass, SIGNAL_CDR_REQUEST, self._request_cdr)
dispatcher_connect(
self.hass, SIGNAL_DISCOVER_PLATFORM, self._discover_platform)
# Only connect after signal connection to ensure we don't miss any
self.client = asteriskClient(host, port, password, self.handle_data)
@callback
def _discover_platform(self, component):
_LOGGER.debug("Adding mailbox %s", component)
self.hass.async_create_task(discovery.async_load_platform(
self.hass, "mailbox", component, {}, self.config))
@callback
def handle_data(self, command, msg):
"""Handle changes to the mailbox."""
from asterisk_mbox.commands import CMD_MESSAGE_LIST
from asterisk_mbox.commands import (CMD_MESSAGE_LIST,
CMD_MESSAGE_CDR_AVAILABLE,
CMD_MESSAGE_CDR)
if command == CMD_MESSAGE_LIST:
_LOGGER.debug("AsteriskVM sent updated message list")
_LOGGER.debug("AsteriskVM sent updated message list: Len %d",
len(msg))
old_messages = self.messages
self.messages = sorted(
msg, key=lambda item: item['info']['origtime'], reverse=True)
async_dispatcher_send(
self.hass, SIGNAL_MESSAGE_UPDATE, self.messages)
if not isinstance(old_messages, list):
async_dispatcher_send(self.hass, SIGNAL_DISCOVER_PLATFORM,
DOMAIN)
async_dispatcher_send(self.hass, SIGNAL_MESSAGE_UPDATE,
self.messages)
elif command == CMD_MESSAGE_CDR:
_LOGGER.debug("AsteriskVM sent updated CDR list: Len %d",
len(msg.get('entries', [])))
self.cdr = msg['entries']
async_dispatcher_send(self.hass, SIGNAL_CDR_UPDATE, self.cdr)
elif command == CMD_MESSAGE_CDR_AVAILABLE:
if not isinstance(self.cdr, list):
_LOGGER.debug("AsteriskVM adding CDR platform")
self.cdr = []
async_dispatcher_send(self.hass, SIGNAL_DISCOVER_PLATFORM,
"asterisk_cdr")
async_dispatcher_send(self.hass, SIGNAL_CDR_REQUEST)
else:
_LOGGER.debug("AsteriskVM sent unknown message '%d' len: %d",
command, len(msg))
@callback
def _request_messages(self):
"""Handle changes to the mailbox."""
_LOGGER.debug("Requesting message list")
self.client.messages()
@callback
def _request_cdr(self):
"""Handle changes to the CDR."""
_LOGGER.debug("Requesting CDR list")
self.client.get_cdr()

View File

@ -1,8 +1,27 @@
{
"mfa_setup": {
"notify": {
"abort": {
"no_available_service": "No hi ha serveis de notificaci\u00f3 disponibles."
},
"error": {
"invalid_code": "Codi inv\u00e0lid, si us plau torni a provar-ho."
},
"step": {
"init": {
"description": "Seleccioneu un dels serveis de notificaci\u00f3:",
"title": "Configureu una contrasenya d'un sol \u00fas a trav\u00e9s del component de notificacions"
},
"setup": {
"description": "**notify.{notify_service}** ha enviat una contrasenya d'un sol \u00fas. Introdu\u00efu-la a continuaci\u00f3:",
"title": "Verifiqueu la configuraci\u00f3"
}
},
"title": "Contrasenya d'un sol \u00fas del servei de notificacions"
},
"totp": {
"error": {
"invalid_code": "Codi no v\u00e0lid, si us plau torni a provar-ho. Si obteniu aquest error repetidament, assegureu-vos que la data i hora de Home Assistant sigui correcta i precisa."
"invalid_code": "Codi inv\u00e0lid, si us plau torni a provar-ho. Si obteniu aquest error repetidament, assegureu-vos que la data i hora de Home Assistant sigui correcta i precisa."
},
"step": {
"init": {

View File

@ -1,8 +1,26 @@
{
"mfa_setup": {
"notify": {
"abort": {
"no_available_service": "Keine Benachrichtigungsdienste verf\u00fcgbar."
},
"error": {
"invalid_code": "Ung\u00fcltiger Code, bitte versuche es erneut."
},
"step": {
"init": {
"description": "Bitte w\u00e4hle einen der Benachrichtigungsdienste:",
"title": "Einmal Passwort f\u00fcr Notify einrichten"
},
"setup": {
"description": "Ein Einmal-Passwort wurde per ** notify gesendet. {notify_service} **. Bitte gebe es unten ein:",
"title": "\u00dcberpr\u00fcfe das Setup"
}
}
},
"totp": {
"error": {
"invalid_code": "Ung\u00fcltiger Code, bitte versuche es erneut. Wenn Sie diesen Fehler regelm\u00e4\u00dfig erhalten, stelle sicher, dass die Uhr deines Home Assistant-Systems korrekt ist."
"invalid_code": "Ung\u00fcltiger Code, bitte versuche es erneut. Wenn du diesen Fehler regelm\u00e4\u00dfig erhalten, stelle sicher, dass die Uhr deines Home Assistant-Systems korrekt ist."
},
"step": {
"init": {

View File

@ -1,5 +1,24 @@
{
"mfa_setup": {
"notify": {
"abort": {
"no_available_service": "No notification services available."
},
"error": {
"invalid_code": "Invalid code, please try again."
},
"step": {
"init": {
"description": "Please select one of the notification services:",
"title": "Set up one-time password delivered by notify component"
},
"setup": {
"description": "A one-time password has been sent via **notify.{notify_service}**. Please enter it below:",
"title": "Verify setup"
}
},
"title": "Notify One-Time Password"
},
"totp": {
"error": {
"invalid_code": "Invalid code, please try again. If you get this error consistently, please make sure the clock of your Home Assistant system is accurate."

View File

@ -1,5 +1,12 @@
{
"mfa_setup": {
"notify": {
"step": {
"setup": {
"description": "Un mot de passe unique a \u00e9t\u00e9 envoy\u00e9 par **notify.{notify_service}**. Veuillez le saisir ci-dessous :"
}
}
},
"totp": {
"error": {
"invalid_code": "Code invalide. Veuillez essayez \u00e0 nouveau. Si cette erreur persiste, assurez-vous que l'horloge de votre syst\u00e8me Home Assistant est correcte."

View File

@ -0,0 +1,35 @@
{
"mfa_setup": {
"notify": {
"abort": {
"no_available_service": "\u05d0\u05d9\u05df \u05e9\u05d9\u05e8\u05d5\u05ea\u05d9 notify \u05d6\u05de\u05d9\u05e0\u05d9\u05dd."
},
"error": {
"invalid_code": "\u05e7\u05d5\u05d3 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9, \u05d0\u05e0\u05d0 \u05e0\u05e1\u05d4 \u05e9\u05d5\u05d1."
},
"step": {
"init": {
"description": "\u05d1\u05d7\u05e8 \u05d0\u05ea \u05d0\u05d7\u05d3 \u05de\u05e9\u05e8\u05d5\u05ea\u05d9 notify",
"title": "\u05d4\u05d2\u05d3\u05e8 \u05e1\u05d9\u05e1\u05de\u05d4 \u05d7\u05d3 \u05e4\u05e2\u05de\u05d9\u05ea \u05d4\u05e0\u05e9\u05dc\u05d7\u05ea \u05e2\u05dc \u05d9\u05d3\u05d9 \u05e8\u05db\u05d9\u05d1 notify"
},
"setup": {
"description": "\u05e1\u05d9\u05e1\u05de\u05d4 \u05d7\u05d3 \u05e4\u05e2\u05de\u05d9\u05ea \u05e0\u05e9\u05dc\u05d7\u05d4 \u05e2\u05dc \u05d9\u05d3\u05d9 **{notify_service}**. \u05d4\u05d6\u05df \u05d0\u05d5\u05ea\u05d4 \u05dc\u05de\u05d8\u05d4:",
"title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d4\u05d4\u05ea\u05e7\u05e0\u05d4"
}
},
"title": "\u05dc\u05d4\u05d5\u05d3\u05d9\u05e2 \u200b\u200b\u05e2\u05dc \u05e1\u05d9\u05e1\u05de\u05d4 \u05d7\u05d3 \u05e4\u05e2\u05de\u05d9\u05ea"
},
"totp": {
"error": {
"invalid_code": "\u05e7\u05d5\u05d3 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9, \u05d0\u05e0\u05d0 \u05e0\u05e1\u05d4 \u05e9\u05d5\u05d1. \u05d0\u05dd \u05d0\u05ea\u05d4 \u05de\u05e7\u05d1\u05dc \u05d0\u05ea \u05d4\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d4\u05d6\u05d5 \u05d1\u05d0\u05d5\u05e4\u05df \u05e2\u05e7\u05d1\u05d9, \u05d5\u05d3\u05d0 \u05e9\u05d4\u05e9\u05e2\u05d5\u05df \u05e9\u05dc \u05de\u05e2\u05e8\u05db\u05ea \u05d4 - Home Assistant \u05e9\u05dc\u05da \u05de\u05d3\u05d5\u05d9\u05e7."
},
"step": {
"init": {
"description": "\u05db\u05d3\u05d9 \u05dc\u05d4\u05e4\u05e2\u05d9\u05dc \u05d0\u05d9\u05de\u05d5\u05ea \u05d3\u05d5 \u05e9\u05dc\u05d1\u05d9 \u05d1\u05d0\u05de\u05e6\u05e2\u05d5\u05ea \u05e1\u05d9\u05e1\u05de\u05d0\u05d5\u05ea \u05d7\u05d3 \u05e4\u05e2\u05de\u05d9\u05d5\u05ea \u05de\u05d1\u05d5\u05e1\u05e1\u05d5\u05ea \u05d6\u05de\u05df, \u05e1\u05e8\u05d5\u05e7 \u05d0\u05ea \u05e7\u05d5\u05d3 QR \u05e2\u05dd \u05d9\u05d9\u05e9\u05d5\u05dd \u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05e9\u05dc\u05da. \u05d0\u05dd \u05d0\u05d9\u05df \u05dc\u05da \u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d6\u05d4, \u05d0\u05e0\u05d5 \u05de\u05de\u05dc\u05d9\u05e6\u05d9\u05dd \u05e2\u05dc [Google Authenticator] (https://support.google.com/accounts/answer/1066447) \u05d0\u05d5 [Authy] (https://authy.com/). \n\n {qr_code} \n \n \u05dc\u05d0\u05d7\u05e8 \u05e1\u05e8\u05d9\u05e7\u05ea \u05d4\u05e7\u05d5\u05d3, \u05d4\u05d6\u05df \u05d0\u05ea \u05d4\u05e7\u05d5\u05d3 \u05d1\u05df \u05e9\u05e9 \u05d4\u05e1\u05e4\u05e8\u05d5\u05ea \u05de\u05d4\u05d0\u05e4\u05dc\u05d9\u05e7\u05e6\u05d9\u05d4 \u05e9\u05dc\u05da \u05db\u05d3\u05d9 \u05dc\u05d0\u05de\u05ea \u05d0\u05ea \u05d4\u05d4\u05d2\u05d3\u05e8\u05d4. \u05d0\u05dd \u05d0\u05ea\u05d4 \u05e0\u05ea\u05e7\u05dc \u05d1\u05d1\u05e2\u05d9\u05d5\u05ea \u05d1\u05e1\u05e8\u05d9\u05e7\u05ea \u05e7\u05d5\u05d3 QR, \u05d1\u05e6\u05e2 \u05d4\u05d2\u05d3\u05e8\u05d4 \u05d9\u05d3\u05e0\u05d9\u05ea \u05e2\u05dd \u05e7\u05d5\u05d3 **`{code}`**.",
"title": "\u05d4\u05d2\u05d3\u05e8 \u05d0\u05d9\u05de\u05d5\u05ea \u05d3\u05d5 \u05e9\u05dc\u05d1\u05d9 \u05d1\u05d0\u05de\u05e6\u05e2\u05d5\u05ea TOTP"
}
},
"title": "TOTP"
}
}
}

View File

@ -0,0 +1,16 @@
{
"mfa_setup": {
"totp": {
"error": {
"invalid_code": "Kode salah, coba lagi. Jika Anda mendapatkan kesalahan ini secara konsisten, pastikan jam pada sistem Home Assistant anda akurat."
},
"step": {
"init": {
"description": "Untuk mengaktifkan otentikasi dua faktor menggunakan password satu kali berbasis waktu, pindai kode QR dengan aplikasi otentikasi Anda. Jika Anda tidak memilikinya, kami menyarankan [Google Authenticator] (https://support.google.com/accounts/answer/1066447) atau [Authy] (https://authy.com/). \n\n {qr_code} \n \n Setelah memindai kode, masukkan kode enam digit dari aplikasi Anda untuk memverifikasi pengaturan. Jika Anda mengalami masalah saat memindai kode QR, lakukan pengaturan manual dengan kode ** ` {code} ` **.",
"title": "Siapkan otentikasi dua faktor menggunakan TOTP"
}
},
"title": "TOTP"
}
}
}

View File

@ -1,5 +1,24 @@
{
"mfa_setup": {
"notify": {
"abort": {
"no_available_service": "\uc0ac\uc6a9 \uac00\ub2a5\ud55c \uc54c\ub9bc \uc11c\ube44\uc2a4\uac00 \uc5c6\uc2b5\ub2c8\ub2e4."
},
"error": {
"invalid_code": "\uc798\ubabb\ub41c \ucf54\ub4dc\uc785\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694."
},
"step": {
"init": {
"description": "\uc54c\ub9bc \uc11c\ube44\uc2a4 \uc911 \ud558\ub098\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694:",
"title": "\uc54c\ub9bc \uad6c\uc131\uc694\uc18c\uac00 \uc81c\uacf5\ud558\ub294 \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638 \uc124\uc815"
},
"setup": {
"description": "**notify.{notify_service}** \uc5d0\uc11c \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638\ub97c \ubcf4\ub0c8\uc2b5\ub2c8\ub2e4. \uc544\ub798\uc758 \uacf5\ub780\uc5d0 \uc785\ub825\ud574 \uc8fc\uc138\uc694:",
"title": "\uc124\uc815 \ud655\uc778"
}
},
"title": "\uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638 \uc54c\ub9bc"
},
"totp": {
"error": {
"invalid_code": "\uc798\ubabb\ub41c \ucf54\ub4dc \uc785\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694. \uc774 \uc624\ub958\uac00 \uc9c0\uc18d\uc801\uc73c\ub85c \ubc1c\uc0dd\ud55c\ub2e4\uba74 Home Assistant \uc758 \uc2dc\uac04\uc124\uc815\uc774 \uc62c\ubc14\ub978\uc9c0 \ud655\uc778\ud574\ubcf4\uc138\uc694."

View File

@ -1,5 +1,24 @@
{
"mfa_setup": {
"notify": {
"abort": {
"no_available_service": "Keen Notifikatioun's D\u00e9ngscht disponibel."
},
"error": {
"invalid_code": "Ong\u00ebltege Code, prob\u00e9iert w.e.g. nach emol."
},
"step": {
"init": {
"description": "Wielt w.e.g. een Notifikatioun's D\u00e9ngscht aus:",
"title": "Eemolegt Passwuert ariichte wat vun engem Notifikatioun's Komponente versch\u00e9ckt g\u00ebtt"
},
"setup": {
"description": "Een eemolegt Passwuert ass vun **notify.{notify_service}** gesch\u00e9ckt ginn. Gitt et w.e.g hei \u00ebnnen dr\u00ebnner an:",
"title": "Astellungen iwwerpr\u00e9iwen"
}
},
"title": "Eemolegt Passwuert Notifikatioun"
},
"totp": {
"error": {
"invalid_code": "Ong\u00ebltege Login, prob\u00e9iert w.e.g. nach emol. Falls d\u00ebse Feeler Message \u00ebmmer er\u00ebm optr\u00ebtt dann iwwerpr\u00e9ift op d'Z\u00e4it vum Home Assistant System richteg ass."

View File

@ -0,0 +1,16 @@
{
"mfa_setup": {
"totp": {
"error": {
"invalid_code": "Ugyldig kode, pr\u00f8v igjen. Dersom du heile tida f\u00e5r denne feilen, m\u00e5 du s\u00f8rge for at klokka p\u00e5 Home Assistant-systemet ditt er n\u00f8yaktig."
},
"step": {
"init": {
"description": "For \u00e5 aktivere tofaktorautentisering ved hjelp av tidsbaserte eingangspassord, skann QR-koden med autentiseringsappen din. Dersom du ikkje har ein, vil vi r\u00e5de deg til \u00e5 bruke anten [Google Authenticator] (https://support.google.com/accounts/answer/1066447) eller [Authy] (https://authy.com/). \n\n {qr_code} \n \nN\u00e5r du har skanna koda, skriv du inn den sekssifra koda fr\u00e5 appen din for \u00e5 stadfeste oppsettet. Dersom du har problemer med \u00e5 skanne QR-koda, gjer du eit manuelt oppsett med kode ** ` {code} ` **.",
"title": "Konfigurer to-faktor-autentisering ved hjelp av TOTP"
}
},
"title": "TOTP"
}
}
}

View File

@ -1,5 +1,24 @@
{
"mfa_setup": {
"notify": {
"abort": {
"no_available_service": "Ingen varslingstjenester er tilgjengelig."
},
"error": {
"invalid_code": "Ugyldig kode, vennligst pr\u00f8v igjen."
},
"step": {
"init": {
"description": "Vennligst velg en av varslingstjenestene:",
"title": "Sett opp engangspassord levert av varsel komponent"
},
"setup": {
"description": "Et engangspassord har blitt sendt via **notify.{notify_service}**. Vennligst skriv det inn nedenfor:",
"title": "Bekreft oppsettet"
}
},
"title": "Varsle engangspassord"
},
"totp": {
"error": {
"invalid_code": "Ugyldig kode, pr\u00f8v igjen. Hvis du f\u00e5r denne feilen konsekvent, m\u00e5 du s\u00f8rge for at klokken p\u00e5 Home Assistant systemet er riktig."

View File

@ -1,5 +1,23 @@
{
"mfa_setup": {
"notify": {
"abort": {
"no_available_service": "Brak dost\u0119pnych us\u0142ug powiadamiania."
},
"error": {
"invalid_code": "Nieprawid\u0142owy kod, spr\u00f3buj ponownie."
},
"step": {
"init": {
"description": "Prosz\u0119 wybra\u0107 jedn\u0105 z us\u0142ugi powiadamiania:",
"title": "Skonfiguruj has\u0142o jednorazowe dostarczone przez komponent powiadomie\u0144"
},
"setup": {
"description": "Has\u0142o jednorazowe zosta\u0142o wys\u0142ane przez ** powiadom. {notify_service} **. Wpisz je poni\u017cej:",
"title": "Sprawd\u017a konfiguracj\u0119"
}
}
},
"totp": {
"error": {
"invalid_code": "Nieprawid\u0142owy kod, spr\u00f3buj ponownie. Je\u015bli b\u0142\u0105d b\u0119dzie si\u0119 powtarza\u0142, upewnij si\u0119, \u017ce czas zegara systemu Home Assistant jest prawid\u0142owy."

View File

@ -0,0 +1,15 @@
{
"mfa_setup": {
"totp": {
"error": {
"invalid_code": "C\u00f3digo inv\u00e1lido, por favor tente novamente. Se voc\u00ea obtiver este erro de forma consistente, certifique-se de que o rel\u00f3gio do sistema Home Assistant esteja correto."
},
"step": {
"init": {
"title": "Configure a autentica\u00e7\u00e3o de dois fatores usando o TOTP"
}
},
"title": "TOTP"
}
}
}

View File

@ -1,5 +1,24 @@
{
"mfa_setup": {
"notify": {
"abort": {
"no_available_service": "\u041d\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u0445 \u0441\u043b\u0443\u0436\u0431 \u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0439."
},
"error": {
"invalid_code": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u044b\u0439 \u043a\u043e\u0434, \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443."
},
"step": {
"init": {
"description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043e\u0434\u043d\u0443 \u0438\u0437 \u0441\u043b\u0443\u0436\u0431 \u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0439:",
"title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043e\u0441\u0442\u0430\u0432\u043a\u0438 \u043e\u0434\u043d\u043e\u0440\u0430\u0437\u043e\u0432\u044b\u0445 \u043f\u0430\u0440\u043e\u043b\u0435\u0439 \u0447\u0435\u0440\u0435\u0437 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0439"
},
"setup": {
"description": "\u041e\u0434\u043d\u043e\u0440\u0430\u0437\u043e\u0432\u044b\u0439 \u043f\u0430\u0440\u043e\u043b\u044c \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d \u0447\u0435\u0440\u0435\u0437 **notify.{notify_service}**. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u0435\u0433\u043e \u043d\u0438\u0436\u0435:",
"title": "\u041f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443"
}
},
"title": "\u0414\u043e\u0441\u0442\u0430\u0432\u043a\u0430 \u043e\u0434\u043d\u043e\u0440\u0430\u0437\u043e\u0432\u044b\u0445 \u043f\u0430\u0440\u043e\u043b\u0435\u0439"
},
"totp": {
"error": {
"invalid_code": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043e\u0434. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430. \u0415\u0441\u043b\u0438 \u0432\u044b \u043f\u043e\u0441\u0442\u043e\u044f\u043d\u043d\u043e \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442\u0435 \u044d\u0442\u0443 \u043e\u0448\u0438\u0431\u043a\u0443, \u043f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0443\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0447\u0430\u0441\u044b \u0432 \u0432\u0430\u0448\u0435\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0435 Home Assistant \u043f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u044e\u0442 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e\u0435 \u0432\u0440\u0435\u043c\u044f."

View File

@ -1,5 +1,24 @@
{
"mfa_setup": {
"notify": {
"abort": {
"no_available_service": "Ni na voljo storitev obve\u0161\u010danja."
},
"error": {
"invalid_code": "Neveljavna koda, poskusite znova."
},
"step": {
"init": {
"description": "Prosimo, izberite eno od storitev obve\u0161\u010danja:",
"title": "Nastavite enkratno geslo, ki ga dostavite z obvestilno komponento"
},
"setup": {
"description": "Enkratno geslo je poslal **notify.{notify_service} **. Vnesite ga spodaj:",
"title": "Preverite nastavitev"
}
},
"title": "Obvesti Enkratno Geslo"
},
"totp": {
"error": {
"invalid_code": "Neveljavna koda, prosimo, poskusite znova. \u010ce dobite to sporo\u010dilo ve\u010dkrat, prosimo poskrbite, da bo ura va\u0161ega Home Assistenta to\u010dna."

View File

@ -1,5 +1,19 @@
{
"mfa_setup": {
"notify": {
"abort": {
"no_available_service": "Inga tillg\u00e4ngliga meddelande tj\u00e4nster."
},
"error": {
"invalid_code": "Ogiltig kod, var god f\u00f6rs\u00f6k igen."
},
"step": {
"setup": {
"description": "Ett eng\u00e5ngsl\u00f6senord har skickats av **notify.{notify_service}**. V\u00e4nligen ange det nedan:",
"title": "Verifiera installationen"
}
}
},
"totp": {
"error": {
"invalid_code": "Ogiltig kod, f\u00f6rs\u00f6k igen. Om du flera g\u00e5nger i rad f\u00e5r detta fel, se till att klockan i din Home Assistant \u00e4r korrekt inst\u00e4lld."

View File

@ -1,5 +1,24 @@
{
"mfa_setup": {
"notify": {
"abort": {
"no_available_service": "\u6ca1\u6709\u53ef\u7528\u7684\u901a\u77e5\u670d\u52a1\u3002"
},
"error": {
"invalid_code": "\u4ee3\u7801\u65e0\u6548\uff0c\u8bf7\u518d\u8bd5\u4e00\u6b21\u3002"
},
"step": {
"init": {
"description": "\u8bf7\u4ece\u4e0b\u9762\u9009\u62e9\u4e00\u4e2a\u901a\u77e5\u670d\u52a1\uff1a",
"title": "\u8bbe\u7f6e\u7531\u901a\u77e5\u7ec4\u4ef6\u4f20\u9012\u7684\u4e00\u6b21\u6027\u5bc6\u7801"
},
"setup": {
"description": "\u4e00\u6b21\u6027\u5bc6\u7801\u5df2\u7531 **notify.{notify_service}** \u53d1\u9001\u3002\u8bf7\u5728\u4e0b\u9762\u8f93\u5165\uff1a",
"title": "\u9a8c\u8bc1\u8bbe\u7f6e"
}
},
"title": "\u4e00\u6b21\u6027\u5bc6\u7801\u901a\u77e5"
},
"totp": {
"error": {
"invalid_code": "\u53e3\u4ee4\u65e0\u6548\uff0c\u8bf7\u91cd\u65b0\u8f93\u5165\u3002\u5982\u679c\u9519\u8bef\u53cd\u590d\u51fa\u73b0\uff0c\u8bf7\u786e\u4fdd Home Assistant \u7cfb\u7edf\u7684\u65f6\u95f4\u51c6\u786e\u65e0\u8bef\u3002"

View File

@ -1,5 +1,24 @@
{
"mfa_setup": {
"notify": {
"abort": {
"no_available_service": "\u6c92\u6709\u53ef\u7528\u7684\u901a\u77e5\u670d\u52d9\u3002"
},
"error": {
"invalid_code": "\u9a57\u8b49\u78bc\u7121\u6548\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002"
},
"step": {
"init": {
"description": "\u8acb\u9078\u64c7\u4e00\u9805\u901a\u77e5\u670d\u52d9\uff1a",
"title": "\u8a2d\u5b9a\u4e00\u6b21\u6027\u5bc6\u78bc\u50b3\u9001\u7d44\u4ef6"
},
"setup": {
"description": "\u4e00\u6b21\u6027\u5bc6\u78bc\u5df2\u900f\u904e **notify.{notify_service}** \u50b3\u9001\u3002\u8acb\u65bc\u4e0b\u65b9\u8f38\u5165\uff1a",
"title": "\u9a57\u8b49\u8a2d\u5b9a"
}
},
"title": "\u901a\u77e5\u4e00\u6b21\u6027\u5bc6\u78bc"
},
"totp": {
"error": {
"invalid_code": "\u9a57\u8b49\u78bc\u7121\u6548\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002\u5047\u5982\u932f\u8aa4\u6301\u7e8c\u767c\u751f\uff0c\u8acb\u5148\u78ba\u5b9a\u60a8\u7684 Home Assistant \u7cfb\u7d71\u4e0a\u7684\u6642\u9593\u8a2d\u5b9a\u6b63\u78ba\u5f8c\uff0c\u518d\u8a66\u4e00\u6b21\u3002"

View File

@ -226,8 +226,9 @@ class LoginFlowResourceView(HomeAssistantView):
if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
# @log_invalid_auth does not work here since it returns HTTP 200
# need manually log failed login attempts
if result['errors'] is not None and \
result['errors'].get('base') == 'invalid_auth':
if (result.get('errors') is not None and
result['errors'].get('base') in ['invalid_auth',
'invalid_code']):
await process_wrong_login(request)
return self.json(_prepare_result_json(result))

View File

@ -11,6 +11,25 @@
"error": {
"invalid_code": "Invalid code, please try again. If you get this error consistently, please make sure the clock of your Home Assistant system is accurate."
}
},
"notify": {
"title": "Notify One-Time Password",
"step": {
"init": {
"title": "Set up one-time password delivered by notify component",
"description": "Please select one of notify service:"
},
"setup": {
"title": "Verify setup",
"description": "A one-time password have sent by **notify.{notify_service}**. Please input it in below:"
}
},
"abort": {
"no_available_service": "No available notify services."
},
"error": {
"invalid_code": "Invalid code, please try again."
}
}
}
}

View File

@ -127,6 +127,7 @@ class DeconzBinarySensor(BinarySensorDevice):
self._sensor.uniqueid.count(':') != 7):
return None
serial = self._sensor.uniqueid.split('-', 1)[0]
bridgeid = self.hass.data[DATA_DECONZ].config.bridgeid
return {
'connections': {(CONNECTION_ZIGBEE, serial)},
'identifiers': {(DECONZ_DOMAIN, serial)},
@ -134,4 +135,5 @@ class DeconzBinarySensor(BinarySensorDevice):
'model': self._sensor.modelid,
'name': self._sensor.name,
'sw_version': self._sensor.swversion,
'via_hub': (DECONZ_DOMAIN, bridgeid),
}

View File

@ -27,17 +27,20 @@ async def async_setup_platform(
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the HomematicIP Cloud binary sensor from a config entry."""
from homematicip.aio.device import (
AsyncShutterContact, AsyncMotionDetectorIndoor, AsyncSmokeDetector)
AsyncShutterContact, AsyncMotionDetectorIndoor, AsyncSmokeDetector,
AsyncWaterSensor, AsyncRotaryHandleSensor)
home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home
devices = []
for device in home.devices:
if isinstance(device, AsyncShutterContact):
if isinstance(device, (AsyncShutterContact, AsyncRotaryHandleSensor)):
devices.append(HomematicipShutterContact(home, device))
elif isinstance(device, AsyncMotionDetectorIndoor):
devices.append(HomematicipMotionDetector(home, device))
elif isinstance(device, AsyncSmokeDetector):
devices.append(HomematicipSmokeDetector(home, device))
elif isinstance(device, AsyncWaterSensor):
devices.append(HomematicipWaterDetector(home, device))
if devices:
async_add_entities(devices)
@ -91,3 +94,17 @@ class HomematicipSmokeDetector(HomematicipGenericDevice, BinarySensorDevice):
def is_on(self):
"""Return true if smoke is detected."""
return self._device.smokeDetectorAlarmType != STATE_SMOKE_OFF
class HomematicipWaterDetector(HomematicipGenericDevice, BinarySensorDevice):
"""Representation of a HomematicIP Cloud water detector."""
@property
def device_class(self):
"""Return the class of this sensor."""
return 'moisture'
@property
def is_on(self):
"""Return true if moisture or waterlevel is detected."""
return self._device.moistureDetected or self._device.waterlevelDetected

View File

@ -18,6 +18,7 @@ from homeassistant.const import (
CONF_HEADERS, CONF_AUTHENTICATION, HTTP_BASIC_AUTHENTICATION,
HTTP_DIGEST_AUTHENTICATION, CONF_DEVICE_CLASS)
import homeassistant.helpers.config_validation as cv
from homeassistant.exceptions import PlatformNotReady
_LOGGER = logging.getLogger(__name__)
@ -66,13 +67,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
rest = RestData(method, resource, auth, headers, payload, verify_ssl)
rest.update()
if rest.data is None:
_LOGGER.error("Unable to fetch REST data from %s", resource)
return False
raise PlatformNotReady
# No need to update the sensor now because it will determine its state
# based in the rest resource that has just been retrieved.
add_entities([RestBinarySensor(
hass, rest, name, device_class, value_template)], True)
hass, rest, name, device_class, value_template)])
class RestBinarySensor(BinarySensorDevice):

View File

@ -4,87 +4,66 @@ Support for Vanderbilt (formerly Siemens) SPC alarm systems.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.spc/
"""
import asyncio
import logging
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.spc import ATTR_DISCOVER_DEVICES, DATA_REGISTRY
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.core import callback
from homeassistant.components.spc import (
ATTR_DISCOVER_DEVICES, SIGNAL_UPDATE_SENSOR)
_LOGGER = logging.getLogger(__name__)
SPC_TYPE_TO_DEVICE_CLASS = {
'0': 'motion',
'1': 'opening',
'3': 'smoke',
}
SPC_INPUT_TO_SENSOR_STATE = {
'0': STATE_OFF,
'1': STATE_ON,
}
def _get_device_class(zone_type):
from pyspcwebgw.const import ZoneType
return {
ZoneType.ALARM: 'motion',
ZoneType.ENTRY_EXIT: 'opening',
ZoneType.FIRE: 'smoke',
}.get(zone_type)
def _get_device_class(spc_type):
"""Get the device class."""
return SPC_TYPE_TO_DEVICE_CLASS.get(spc_type, None)
def _get_sensor_state(spc_input):
"""Get the sensor state."""
return SPC_INPUT_TO_SENSOR_STATE.get(spc_input, STATE_UNAVAILABLE)
def _create_sensor(hass, zone):
"""Create a SPC sensor."""
return SpcBinarySensor(
zone_id=zone['id'], name=zone['zone_name'],
state=_get_sensor_state(zone['input']),
device_class=_get_device_class(zone['type']),
spc_registry=hass.data[DATA_REGISTRY])
@asyncio.coroutine
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 SPC binary sensor."""
if (discovery_info is None or
discovery_info[ATTR_DISCOVER_DEVICES] is None):
return
async_add_entities(
_create_sensor(hass, zone)
for zone in discovery_info[ATTR_DISCOVER_DEVICES]
if _get_device_class(zone['type']))
async_add_entities(SpcBinarySensor(zone)
for zone in discovery_info[ATTR_DISCOVER_DEVICES]
if _get_device_class(zone.type))
class SpcBinarySensor(BinarySensorDevice):
"""Representation of a sensor based on a SPC zone."""
def __init__(self, zone_id, name, state, device_class, spc_registry):
def __init__(self, zone):
"""Initialize the sensor device."""
self._zone_id = zone_id
self._name = name
self._state = state
self._device_class = device_class
self._zone = zone
spc_registry.register_sensor_device(zone_id, self)
async def async_added_to_hass(self):
"""Call for adding new entities."""
async_dispatcher_connect(self.hass,
SIGNAL_UPDATE_SENSOR.format(self._zone.id),
self._update_callback)
@asyncio.coroutine
def async_update_from_spc(self, state, extra):
"""Update the state of the device."""
self._state = state
yield from self.async_update_ha_state()
@callback
def _update_callback(self):
"""Call update method."""
self.async_schedule_update_ha_state(True)
@property
def name(self):
"""Return the name of the device."""
return self._name
return self._zone.name
@property
def is_on(self):
"""Whether the device is switched on."""
return self._state == STATE_ON
from pyspcwebgw.const import ZoneInput
return self._zone.input == ZoneInput.OPEN
@property
def hidden(self) -> bool:
@ -100,4 +79,4 @@ class SpcBinarySensor(BinarySensorDevice):
@property
def device_class(self):
"""Return the device class."""
return self._device_class
return _get_device_class(self._zone.type)

View File

@ -14,9 +14,6 @@ from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA)
from homeassistant.components.wirelesstag import (
DOMAIN as WIRELESSTAG_DOMAIN,
WIRELESSTAG_TYPE_13BIT, WIRELESSTAG_TYPE_WATER,
WIRELESSTAG_TYPE_ALSPRO,
WIRELESSTAG_TYPE_WEMO_DEVICE,
SIGNAL_BINARY_EVENT_UPDATE,
WirelessTagBaseSensor)
from homeassistant.const import (
@ -30,7 +27,7 @@ _LOGGER = logging.getLogger(__name__)
# On means in range, Off means out of range
SENSOR_PRESENCE = 'presence'
# On means motion detected, Off means cear
# On means motion detected, Off means clear
SENSOR_MOTION = 'motion'
# On means open, Off means closed
@ -55,49 +52,21 @@ SENSOR_LIGHT = 'light'
SENSOR_MOISTURE = 'moisture'
# On means tag battery is low, Off means normal
SENSOR_BATTERY = 'low_battery'
SENSOR_BATTERY = 'battery'
# Sensor types: Name, device_class, push notification type representing 'on',
# attr to check
SENSOR_TYPES = {
SENSOR_PRESENCE: ['Presence', 'presence', 'is_in_range', {
"on": "oor",
"off": "back_in_range"
}, 2],
SENSOR_MOTION: ['Motion', 'motion', 'is_moved', {
"on": "motion_detected",
}, 5],
SENSOR_DOOR: ['Door', 'door', 'is_door_open', {
"on": "door_opened",
"off": "door_closed"
}, 5],
SENSOR_COLD: ['Cold', 'cold', 'is_cold', {
"on": "temp_toolow",
"off": "temp_normal"
}, 4],
SENSOR_HEAT: ['Heat', 'heat', 'is_heat', {
"on": "temp_toohigh",
"off": "temp_normal"
}, 4],
SENSOR_DRY: ['Too dry', 'dry', 'is_too_dry', {
"on": "too_dry",
"off": "cap_normal"
}, 2],
SENSOR_WET: ['Too wet', 'wet', 'is_too_humid', {
"on": "too_humid",
"off": "cap_normal"
}, 2],
SENSOR_LIGHT: ['Light', 'light', 'is_light_on', {
"on": "too_bright",
"off": "light_normal"
}, 1],
SENSOR_MOISTURE: ['Leak', 'moisture', 'is_leaking', {
"on": "water_detected",
"off": "water_dried",
}, 1],
SENSOR_BATTERY: ['Low Battery', 'battery', 'is_battery_low', {
"on": "low_battery"
}, 3]
SENSOR_PRESENCE: 'Presence',
SENSOR_MOTION: 'Motion',
SENSOR_DOOR: 'Door',
SENSOR_COLD: 'Cold',
SENSOR_HEAT: 'Heat',
SENSOR_DRY: 'Too dry',
SENSOR_WET: 'Too wet',
SENSOR_LIGHT: 'Light',
SENSOR_MOISTURE: 'Leak',
SENSOR_BATTERY: 'Low Battery'
}
@ -114,7 +83,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
sensors = []
tags = platform.tags
for tag in tags.values():
allowed_sensor_types = WirelessTagBinarySensor.allowed_sensors(tag)
allowed_sensor_types = tag.supported_binary_events_types
for sensor_type in config.get(CONF_MONITORED_CONDITIONS):
if sensor_type in allowed_sensor_types:
sensors.append(WirelessTagBinarySensor(platform, tag,
@ -127,59 +96,21 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
class WirelessTagBinarySensor(WirelessTagBaseSensor, BinarySensorDevice):
"""A binary sensor implementation for WirelessTags."""
@classmethod
def allowed_sensors(cls, tag):
"""Return list of allowed sensor types for specific tag type."""
sensors_map = {
# 13-bit tag - allows everything but not light and moisture
WIRELESSTAG_TYPE_13BIT: [
SENSOR_PRESENCE, SENSOR_BATTERY,
SENSOR_MOTION, SENSOR_DOOR,
SENSOR_COLD, SENSOR_HEAT,
SENSOR_DRY, SENSOR_WET],
# Moister/water sensor - temperature and moisture only
WIRELESSTAG_TYPE_WATER: [
SENSOR_PRESENCE, SENSOR_BATTERY,
SENSOR_COLD, SENSOR_HEAT,
SENSOR_MOISTURE],
# ALS Pro: allows everything, but not moisture
WIRELESSTAG_TYPE_ALSPRO: [
SENSOR_PRESENCE, SENSOR_BATTERY,
SENSOR_MOTION, SENSOR_DOOR,
SENSOR_COLD, SENSOR_HEAT,
SENSOR_DRY, SENSOR_WET,
SENSOR_LIGHT],
# Wemo are power switches.
WIRELESSTAG_TYPE_WEMO_DEVICE: [SENSOR_PRESENCE]
}
# allow everything if tag type is unknown
# (i just dont have full catalog of them :))
tag_type = tag.tag_type
fullset = SENSOR_TYPES.keys()
return sensors_map[tag_type] if tag_type in sensors_map else fullset
def __init__(self, api, tag, sensor_type):
"""Initialize a binary sensor for a Wireless Sensor Tags."""
super().__init__(api, tag)
self._sensor_type = sensor_type
self._name = '{0} {1}'.format(self._tag.name,
SENSOR_TYPES[self._sensor_type][0])
self._device_class = SENSOR_TYPES[self._sensor_type][1]
self._tag_attr = SENSOR_TYPES[self._sensor_type][2]
self.binary_spec = SENSOR_TYPES[self._sensor_type][3]
self.tag_id_index_template = SENSOR_TYPES[self._sensor_type][4]
self.event.human_readable_name)
async def async_added_to_hass(self):
"""Register callbacks."""
tag_id = self.tag_id
event_type = self.device_class
mac = self.tag_manager_mac
async_dispatcher_connect(
self.hass,
SIGNAL_BINARY_EVENT_UPDATE.format(tag_id, event_type),
SIGNAL_BINARY_EVENT_UPDATE.format(tag_id, event_type, mac),
self._on_binary_event_callback)
@property
@ -190,7 +121,12 @@ class WirelessTagBinarySensor(WirelessTagBaseSensor, BinarySensorDevice):
@property
def device_class(self):
"""Return the class of the binary sensor."""
return self._device_class
return self._sensor_type
@property
def event(self):
"""Binary event of tag."""
return self._tag.event[self._sensor_type]
@property
def principal_value(self):
@ -198,9 +134,7 @@ class WirelessTagBinarySensor(WirelessTagBaseSensor, BinarySensorDevice):
Subclasses need override based on type of sensor.
"""
return (
STATE_ON if getattr(self._tag, self._tag_attr, False)
else STATE_OFF)
return STATE_ON if self.event.is_state_on else STATE_OFF
def updated_state_value(self):
"""Use raw princial value."""
@ -208,7 +142,7 @@ class WirelessTagBinarySensor(WirelessTagBaseSensor, BinarySensorDevice):
@callback
def _on_binary_event_callback(self, event):
"""Update state from arrive push notification."""
"""Update state from arrived push notification."""
# state should be 'on' or 'off'
self._state = event.data.get('state')
self.async_schedule_update_ha_state()

View File

@ -15,35 +15,38 @@ from homeassistant.const import CONF_NAME, WEEKDAYS
from homeassistant.components.binary_sensor import BinarySensorDevice
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['holidays==0.9.7']
REQUIREMENTS = ['holidays==0.9.6']
_LOGGER = logging.getLogger(__name__)
# List of all countries currently supported by holidays
# There seems to be no way to get the list out at runtime
ALL_COUNTRIES = ['Argentina', 'AR', 'Australia', 'AU', 'Austria', 'AT',
'Belgium', 'BE', 'Canada', 'CA', 'Colombia', 'CO', 'Czech',
'CZ', 'Denmark', 'DK', 'England', 'EuropeanCentralBank',
'ECB', 'TAR', 'Finland', 'FI', 'France', 'FRA', 'Germany',
'DE', 'Hungary', 'HU', 'India', 'IND', 'Ireland',
'Isle of Man', 'Italy', 'IT', 'Japan', 'JP', 'Mexico', 'MX',
'Netherlands', 'NL', 'NewZealand', 'NZ', 'Northern Ireland',
'Norway', 'NO', 'Polish', 'PL', 'Portugal', 'PT',
'PortugalExt', 'PTE', 'Scotland', 'Slovenia', 'SI',
'Slovakia', 'SK', 'South Africa', 'ZA', 'Spain', 'ES',
'Sweden', 'SE', 'Switzerland', 'CH', 'UnitedKingdom', 'UK',
'UnitedStates', 'US', 'Wales']
ALL_COUNTRIES = [
'Argentina', 'AR', 'Australia', 'AU', 'Austria', 'AT', 'Belarus', 'BY'
'Belgium', 'BE', 'Canada', 'CA', 'Colombia', 'CO', 'Czech', 'CZ',
'Denmark', 'DK', 'England', 'EuropeanCentralBank', 'ECB', 'TAR',
'Finland', 'FI', 'France', 'FRA', 'Germany', 'DE', 'Hungary', 'HU',
'India', 'IND', 'Ireland', 'Isle of Man', 'Italy', 'IT', 'Japan', 'JP',
'Mexico', 'MX', 'Netherlands', 'NL', 'NewZealand', 'NZ',
'Northern Ireland', 'Norway', 'NO', 'Polish', 'PL', 'Portugal', 'PT',
'PortugalExt', 'PTE', 'Scotland', 'Slovenia', 'SI', 'Slovakia', 'SK',
'South Africa', 'ZA', 'Spain', 'ES', 'Sweden', 'SE', 'Switzerland', 'CH',
'UnitedKingdom', 'UK', 'UnitedStates', 'US', 'Wales',
]
ALLOWED_DAYS = WEEKDAYS + ['holiday']
CONF_COUNTRY = 'country'
CONF_PROVINCE = 'province'
CONF_WORKDAYS = 'workdays'
CONF_EXCLUDES = 'excludes'
CONF_OFFSET = 'days_offset'
# By default, Monday - Friday are workdays
DEFAULT_WORKDAYS = ['mon', 'tue', 'wed', 'thu', 'fri']
CONF_EXCLUDES = 'excludes'
# By default, public holidays, Saturdays and Sundays are excluded from workdays
DEFAULT_EXCLUDES = ['sat', 'sun', 'holiday']
DEFAULT_NAME = 'Workday Sensor'
ALLOWED_DAYS = WEEKDAYS + ['holiday']
CONF_OFFSET = 'days_offset'
DEFAULT_OFFSET = 0
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@ -86,7 +89,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
else:
_LOGGER.error("There is no province/state %s in country %s",
province, country)
return False
return
_LOGGER.debug("Found the following holidays for your configuration:")
for date, name in sorted(obj_holidays.items()):

View File

@ -65,28 +65,25 @@ async def _async_setup_iaszone(hass, config, async_add_entities,
async def _async_setup_remote(hass, config, async_add_entities,
discovery_info):
async def safe(coro):
"""Run coro, catching ZigBee delivery errors, and ignoring them."""
import zigpy.exceptions
try:
await coro
except zigpy.exceptions.DeliveryError as exc:
_LOGGER.warning("Ignoring error during setup: %s", exc)
remote = Remote(**discovery_info)
if discovery_info['new_join']:
from zigpy.zcl.clusters.general import OnOff, LevelControl
out_clusters = discovery_info['out_clusters']
if OnOff.cluster_id in out_clusters:
cluster = out_clusters[OnOff.cluster_id]
await safe(cluster.bind())
await safe(cluster.configure_reporting(0, 0, 600, 1))
await zha.configure_reporting(
remote.entity_id, cluster, 0, min_report=0, max_report=600,
reportable_change=1
)
if LevelControl.cluster_id in out_clusters:
cluster = out_clusters[LevelControl.cluster_id]
await safe(cluster.bind())
await safe(cluster.configure_reporting(0, 1, 600, 1))
await zha.configure_reporting(
remote.entity_id, cluster, 0, min_report=1, max_report=600,
reportable_change=1
)
sensor = Switch(**discovery_info)
async_add_entities([sensor], update_before_add=True)
async_add_entities([remote], update_before_add=True)
class BinarySensor(zha.Entity, BinarySensorDevice):
@ -131,17 +128,18 @@ class BinarySensor(zha.Entity, BinarySensorDevice):
async def async_update(self):
"""Retrieve latest state."""
from bellows.types.basic import uint16_t
from zigpy.types.basic import uint16_t
result = await zha.safe_read(self._endpoint.ias_zone,
['zone_status'],
allow_cache=False)
allow_cache=False,
only_cache=(not self._initialized))
state = result.get('zone_status', self._state)
if isinstance(state, (int, uint16_t)):
self._state = result.get('zone_status', self._state) & 3
class Switch(zha.Entity, BinarySensorDevice):
class Remote(zha.Entity, BinarySensorDevice):
"""ZHA switch/remote controller/button."""
_domain = DOMAIN

View File

@ -9,8 +9,8 @@ import datetime
import homeassistant.util.dt as dt_util
from homeassistant.helpers.event import track_point_in_time
from homeassistant.components import zwave
from homeassistant.components.zwave import workaround
from homeassistant.components.zwave import async_setup_platform # noqa pylint: disable=unused-import
from homeassistant.components.zwave import ( # noqa pylint: disable=unused-import
async_setup_platform, workaround)
from homeassistant.components.binary_sensor import (
DOMAIN,
BinarySensorDevice)

View File

@ -14,7 +14,7 @@ from homeassistant.helpers import discovery
from homeassistant.helpers.event import track_utc_time_change
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['bimmer_connected==0.5.1']
REQUIREMENTS = ['bimmer_connected==0.5.2']
_LOGGER = logging.getLogger(__name__)

View File

@ -50,7 +50,7 @@ class AxisCamera(MjpegCamera):
def __init__(self, hass, config, port):
"""Initialize Axis Communications camera component."""
super().__init__(hass, config)
super().__init__(config)
self.port = port
dispatcher_connect(
hass, DOMAIN + '_' + config[CONF_NAME] + '_new_ip', self._new_ip)

View File

@ -0,0 +1,210 @@
"""
This component provides support to the Logi Circle camera.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.logi_circle/
"""
import logging
import asyncio
from datetime import timedelta
import voluptuous as vol
from homeassistant.helpers import config_validation as cv
from homeassistant.components.logi_circle import (
DOMAIN as LOGI_CIRCLE_DOMAIN, CONF_ATTRIBUTION)
from homeassistant.components.camera import (
Camera, PLATFORM_SCHEMA, CAMERA_SERVICE_SCHEMA, SUPPORT_ON_OFF,
ATTR_ENTITY_ID, ATTR_FILENAME, DOMAIN)
from homeassistant.const import (
ATTR_ATTRIBUTION, ATTR_BATTERY_CHARGING, ATTR_BATTERY_LEVEL,
CONF_SCAN_INTERVAL, STATE_ON, STATE_OFF)
DEPENDENCIES = ['logi_circle']
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=60)
SERVICE_SET_CONFIG = 'logi_circle_set_config'
SERVICE_LIVESTREAM_SNAPSHOT = 'logi_circle_livestream_snapshot'
SERVICE_LIVESTREAM_RECORD = 'logi_circle_livestream_record'
DATA_KEY = 'camera.logi_circle'
BATTERY_SAVING_MODE_KEY = 'BATTERY_SAVING'
PRIVACY_MODE_KEY = 'PRIVACY_MODE'
LED_MODE_KEY = 'LED'
ATTR_MODE = 'mode'
ATTR_VALUE = 'value'
ATTR_DURATION = 'duration'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL):
cv.time_period,
})
LOGI_CIRCLE_SERVICE_SET_CONFIG = CAMERA_SERVICE_SCHEMA.extend({
vol.Required(ATTR_MODE): vol.In([BATTERY_SAVING_MODE_KEY, LED_MODE_KEY,
PRIVACY_MODE_KEY]),
vol.Required(ATTR_VALUE): cv.boolean
})
LOGI_CIRCLE_SERVICE_SNAPSHOT = CAMERA_SERVICE_SCHEMA.extend({
vol.Required(ATTR_FILENAME): cv.template
})
LOGI_CIRCLE_SERVICE_RECORD = CAMERA_SERVICE_SCHEMA.extend({
vol.Required(ATTR_FILENAME): cv.template,
vol.Required(ATTR_DURATION): cv.positive_int
})
async def async_setup_platform(
hass, config, async_add_entities, discovery_info=None):
"""Set up a Logi Circle Camera."""
devices = hass.data[LOGI_CIRCLE_DOMAIN]
cameras = []
for device in devices:
cameras.append(LogiCam(device, config))
async_add_entities(cameras, True)
async def service_handler(service):
"""Dispatch service calls to target entities."""
params = {key: value for key, value in service.data.items()
if key != ATTR_ENTITY_ID}
entity_ids = service.data.get(ATTR_ENTITY_ID)
if entity_ids:
target_devices = [dev for dev in cameras
if dev.entity_id in entity_ids]
else:
target_devices = cameras
for target_device in target_devices:
if service.service == SERVICE_SET_CONFIG:
await target_device.set_config(**params)
if service.service == SERVICE_LIVESTREAM_SNAPSHOT:
await target_device.livestream_snapshot(**params)
if service.service == SERVICE_LIVESTREAM_RECORD:
await target_device.download_livestream(**params)
hass.services.async_register(
DOMAIN, SERVICE_SET_CONFIG, service_handler,
schema=LOGI_CIRCLE_SERVICE_SET_CONFIG)
hass.services.async_register(
DOMAIN, SERVICE_LIVESTREAM_SNAPSHOT, service_handler,
schema=LOGI_CIRCLE_SERVICE_SNAPSHOT)
hass.services.async_register(
DOMAIN, SERVICE_LIVESTREAM_RECORD, service_handler,
schema=LOGI_CIRCLE_SERVICE_RECORD)
class LogiCam(Camera):
"""An implementation of a Logi Circle camera."""
def __init__(self, camera, device_info):
"""Initialize Logi Circle camera."""
super().__init__()
self._camera = camera
self._name = self._camera.name
self._id = self._camera.mac_address
self._has_battery = self._camera.supports_feature('battery_level')
@property
def unique_id(self):
"""Return a unique ID."""
return self._id
@property
def name(self):
"""Return the name of this camera."""
return self._name
@property
def supported_features(self):
"""Logi Circle camera's support turning on and off ("soft" switch)."""
return SUPPORT_ON_OFF
@property
def device_state_attributes(self):
"""Return the state attributes."""
state = {
ATTR_ATTRIBUTION: CONF_ATTRIBUTION,
'battery_saving_mode': (
STATE_ON if self._camera.battery_saving else STATE_OFF),
'ip_address': self._camera.ip_address,
'microphone_gain': self._camera.microphone_gain
}
# Add battery attributes if camera is battery-powered
if self._has_battery:
state[ATTR_BATTERY_CHARGING] = self._camera.is_charging
state[ATTR_BATTERY_LEVEL] = self._camera.battery_level
return state
async def async_camera_image(self):
"""Return a still image from the camera."""
return await self._camera.get_snapshot_image()
async def async_turn_off(self):
"""Disable streaming mode for this camera."""
await self._camera.set_streaming_mode(False)
async def async_turn_on(self):
"""Enable streaming mode for this camera."""
await self._camera.set_streaming_mode(True)
@property
def should_poll(self):
"""Update the image periodically."""
return True
async def set_config(self, mode, value):
"""Set an configuration property for the target camera."""
if mode == LED_MODE_KEY:
await self._camera.set_led(value)
if mode == PRIVACY_MODE_KEY:
await self._camera.set_privacy_mode(value)
if mode == BATTERY_SAVING_MODE_KEY:
await self._camera.set_battery_saving_mode(value)
async def download_livestream(self, filename, duration):
"""Download a recording from the camera's livestream."""
# Render filename from template.
filename.hass = self.hass
stream_file = filename.async_render(
variables={ATTR_ENTITY_ID: self.entity_id})
# Respect configured path whitelist.
if not self.hass.config.is_allowed_path(stream_file):
_LOGGER.error(
"Can't write %s, no access to path!", stream_file)
return
asyncio.shield(self._camera.record_livestream(
stream_file, timedelta(seconds=duration)), loop=self.hass.loop)
async def livestream_snapshot(self, filename):
"""Download a still frame from the camera's livestream."""
# Render filename from template.
filename.hass = self.hass
snapshot_file = filename.async_render(
variables={ATTR_ENTITY_ID: self.entity_id})
# Respect configured path whitelist.
if not self.hass.config.is_allowed_path(snapshot_file):
_LOGGER.error(
"Can't write %s, no access to path!", snapshot_file)
return
asyncio.shield(self._camera.get_livestream_image(
snapshot_file), loop=self.hass.loop)
async def async_update(self):
"""Update camera entity and refresh attributes."""
await self._camera.update()

View File

@ -47,7 +47,7 @@ def async_setup_platform(hass, config, async_add_entities,
"""Set up a MJPEG IP Camera."""
if discovery_info:
config = PLATFORM_SCHEMA(discovery_info)
async_add_entities([MjpegCamera(hass, config)])
async_add_entities([MjpegCamera(config)])
def extract_image_from_mjpeg(stream):
@ -65,7 +65,7 @@ def extract_image_from_mjpeg(stream):
class MjpegCamera(Camera):
"""An implementation of an IP camera that is reachable over a URL."""
def __init__(self, hass, device_info):
def __init__(self, device_info):
"""Initialize a MJPEG camera."""
super().__init__()
self._name = device_info.get(CONF_NAME)

View File

@ -19,12 +19,14 @@ from homeassistant.helpers import config_validation as cv
_LOGGER = logging.getLogger(__name__)
CONF_TOPIC = 'topic'
CONF_UNIQUE_ID = 'unique_id'
DEFAULT_NAME = 'MQTT Camera'
DEPENDENCIES = ['mqtt']
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_TOPIC): mqtt.valid_subscribe_topic,
vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string
})
@ -38,6 +40,7 @@ def async_setup_platform(hass, config, async_add_entities,
async_add_entities([MqttCamera(
config.get(CONF_NAME),
config.get(CONF_UNIQUE_ID),
config.get(CONF_TOPIC)
)])
@ -45,11 +48,12 @@ def async_setup_platform(hass, config, async_add_entities,
class MqttCamera(Camera):
"""representation of a MQTT camera."""
def __init__(self, name, topic):
def __init__(self, name, unique_id, topic):
"""Initialize the MQTT Camera."""
super().__init__()
self._name = name
self._unique_id = unique_id
self._topic = topic
self._qos = 0
self._last_image = None
@ -64,6 +68,11 @@ class MqttCamera(Camera):
"""Return the name of this camera."""
return self._name
@property
def unique_id(self):
"""Return a unique ID."""
return self._unique_id
@asyncio.coroutine
def async_added_to_hass(self):
"""Subscribe MQTT events."""

View File

@ -62,6 +62,23 @@ class NestCamera(Camera):
"""Return the name of the nest, if any."""
return self._name
@property
def unique_id(self):
"""Return the serial number."""
return self.device.device_id
@property
def device_info(self):
"""Return information about the device."""
return {
'identifiers': {
(nest.DOMAIN, self.device.device_id)
},
'name': self.device.name_long,
'manufacturer': 'Nest Labs',
'model': "Camera",
}
@property
def should_poll(self):
"""Nest camera should poll periodically."""

View File

@ -39,9 +39,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up a Ring Door Bell and StickUp Camera."""
ring = hass.data[DATA_RING]
@ -67,14 +65,14 @@ def async_setup_platform(hass, config, async_add_entities,
''' following cameras: {}.'''.format(cameras)
_LOGGER.error(err_msg)
hass.components.persistent_notification.async_create(
hass.components.persistent_notification.create(
'Error: {}<br />'
'You will need to restart hass after fixing.'
''.format(err_msg),
title=NOTIFICATION_TITLE,
notification_id=NOTIFICATION_ID)
async_add_entities(cams, True)
add_entities(cams, True)
return True

View File

@ -63,3 +63,39 @@ onvif_ptz:
zoom:
description: "Zoom. Allowed values: ZOOM_IN, ZOOM_OUT"
example: "ZOOM_IN"
logi_circle_set_config:
description: Set a configuration property.
fields:
entity_id:
description: Name(s) of entities to apply the operation mode to.
example: "camera.living_room_camera"
mode:
description: "Operation mode. Allowed values: BATTERY_SAVING, LED, PRIVACY_MODE."
example: "PRIVACY_MODE"
value:
description: "Operation value. Allowed values: true, false"
example: true
logi_circle_livestream_snapshot:
description: Take a snapshot from the camera's livestream. Will wake the camera from sleep if required.
fields:
entity_id:
description: Name(s) of entities to create snapshots from.
example: "camera.living_room_camera"
filename:
description: Template of a Filename. Variable is entity_id.
example: "/tmp/snapshot_{{ entity_id }}.jpg"
logi_circle_livestream_record:
description: Take a video recording from the camera's livestream.
fields:
entity_id:
description: Name(s) of entities to create recordings from.
example: "camera.living_room_camera"
filename:
description: Template of a Filename. Variable is entity_id.
example: "/tmp/snapshot_{{ entity_id }}.mp4"
duration:
description: Recording duration in seconds.
example: 60

View File

@ -4,91 +4,47 @@ Support for ZoneMinder camera streaming.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.zoneminder/
"""
import asyncio
import logging
from urllib.parse import urljoin, urlencode
from homeassistant.const import CONF_NAME
from homeassistant.components.camera.mjpeg import (
CONF_MJPEG_URL, CONF_STILL_IMAGE_URL, MjpegCamera)
from homeassistant.components import zoneminder
from homeassistant.components.zoneminder import DOMAIN as ZONEMINDER_DOMAIN
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['zoneminder']
DOMAIN = 'zoneminder'
# From ZoneMinder's web/includes/config.php.in
ZM_STATE_ALARM = "2"
def _get_image_url(hass, monitor, mode):
zm_data = hass.data[DOMAIN]
query = urlencode({
'mode': mode,
'buffer': monitor['StreamReplayBuffer'],
'monitor': monitor['Id'],
})
url = '{zms_url}?{query}'.format(
zms_url=urljoin(zm_data['server_origin'], zm_data['path_zms']),
query=query,
)
_LOGGER.debug('Monitor %s %s URL (without auth): %s',
monitor['Id'], mode, url)
if not zm_data['username']:
return url
url += '&user={:s}'.format(zm_data['username'])
if not zm_data['password']:
return url
return url + '&pass={:s}'.format(zm_data['password'])
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the ZoneMinder cameras."""
cameras = []
monitors = zoneminder.get_state('api/monitors.json')
zm_client = hass.data[ZONEMINDER_DOMAIN]
monitors = zm_client.get_monitors()
if not monitors:
_LOGGER.warning("Could not fetch monitors from ZoneMinder")
return
for i in monitors['monitors']:
monitor = i['Monitor']
if monitor['Function'] == 'None':
_LOGGER.info("Skipping camera %s", monitor['Id'])
continue
_LOGGER.info("Initializing camera %s", monitor['Id'])
device_info = {
CONF_NAME: monitor['Name'],
CONF_MJPEG_URL: _get_image_url(hass, monitor, 'jpeg'),
CONF_STILL_IMAGE_URL: _get_image_url(hass, monitor, 'single')
}
cameras.append(ZoneMinderCamera(hass, device_info, monitor))
if not cameras:
_LOGGER.warning("No active cameras found")
return
async_add_entities(cameras)
cameras = []
for monitor in monitors:
_LOGGER.info("Initializing camera %s", monitor.id)
cameras.append(ZoneMinderCamera(monitor))
add_entities(cameras)
class ZoneMinderCamera(MjpegCamera):
"""Representation of a ZoneMinder Monitor Stream."""
def __init__(self, hass, device_info, monitor):
def __init__(self, monitor):
"""Initialize as a subclass of MjpegCamera."""
super().__init__(hass, device_info)
self._monitor_id = int(monitor['Id'])
device_info = {
CONF_NAME: monitor.name,
CONF_MJPEG_URL: monitor.mjpeg_image_url,
CONF_STILL_IMAGE_URL: monitor.still_image_url
}
super().__init__(device_info)
self._is_recording = None
self._monitor = monitor
@property
def should_poll(self):
@ -97,17 +53,8 @@ class ZoneMinderCamera(MjpegCamera):
def update(self):
"""Update our recording state from the ZM API."""
_LOGGER.debug("Updating camera state for monitor %i", self._monitor_id)
status_response = zoneminder.get_state(
'api/monitors/alarm/id:%i/command:status.json' % self._monitor_id
)
if not status_response:
_LOGGER.warning("Could not get status for monitor %i",
self._monitor_id)
return
self._is_recording = status_response.get('status') == ZM_STATE_ALARM
_LOGGER.debug("Updating camera state for monitor %i", self._monitor.id)
self._is_recording = self._monitor.is_recording
@property
def is_recording(self):

View File

@ -6,7 +6,7 @@
},
"step": {
"confirm": {
"description": "M\u00f6chten Sie Google Cast einrichten?",
"description": "M\u00f6chtest du Google Cast einrichten?",
"title": "Google Cast"
}
},

View File

@ -0,0 +1,15 @@
{
"config": {
"abort": {
"no_devices_found": "Tidak ada perangkat Google Cast yang ditemukan pada jaringan.",
"single_instance_allowed": "Hanya satu konfigurasi Google Cast yang diperlukan."
},
"step": {
"confirm": {
"description": "Apakah Anda ingin menyiapkan Google Cast?",
"title": "Google Cast"
}
},
"title": "Google Cast"
}
}

View File

@ -0,0 +1,15 @@
{
"config": {
"abort": {
"no_devices_found": "Klar",
"single_instance_allowed": "Du treng berre \u00e5 sette opp \u00e9in Google Cast-konfigurasjon."
},
"step": {
"confirm": {
"description": "Vil du sette opp Google Cast?",
"title": "Google Cast"
}
},
"title": "Google Cast"
}
}

View File

@ -35,4 +35,5 @@ async def _async_has_devices(hass):
config_entry_flow.register_discovery_flow(
DOMAIN, 'Google Cast', _async_has_devices)
DOMAIN, 'Google Cast', _async_has_devices,
config_entries.CONN_CLASS_LOCAL_PUSH)

View File

@ -18,7 +18,7 @@ from homeassistant.const import (
TEMP_FAHRENHEIT)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pyeconet==0.0.5']
REQUIREMENTS = ['pyeconet==0.0.6']
_LOGGER = logging.getLogger(__name__)

View File

@ -72,8 +72,9 @@ class OpenThermGateway(ClimateDevice):
"""Receive and handle a new report from the Gateway."""
_LOGGER.debug("Received report: %s", status)
ch_active = status.get(self.pyotgw.DATA_SLAVE_CH_ACTIVE)
flame_on = status.get(self.pyotgw.DATA_SLAVE_FLAME_ON)
cooling_active = status.get(self.pyotgw.DATA_SLAVE_COOLING_ACTIVE)
if ch_active:
if ch_active and flame_on:
self._current_operation = STATE_HEAT
elif cooling_active:
self._current_operation = STATE_COOL

View File

@ -118,7 +118,7 @@ class WinkThermostat(WinkDevice, ClimateDevice):
self.hass, self.target_temperature_low, self.temperature_unit,
PRECISION_TENTHS)
if self.external_temperature:
if self.external_temperature is not None:
data[ATTR_EXTERNAL_TEMPERATURE] = show_temp(
self.hass, self.external_temperature, self.temperature_unit,
PRECISION_TENTHS)
@ -126,16 +126,16 @@ class WinkThermostat(WinkDevice, ClimateDevice):
if self.smart_temperature:
data[ATTR_SMART_TEMPERATURE] = self.smart_temperature
if self.occupied:
if self.occupied is not None:
data[ATTR_OCCUPIED] = self.occupied
if self.eco_target:
if self.eco_target is not None:
data[ATTR_ECO_TARGET] = self.eco_target
if self.heat_on:
if self.heat_on is not None:
data[ATTR_HEAT_ON] = self.heat_on
if self.cool_on:
if self.cool_on is not None:
data[ATTR_COOL_ON] = self.cool_on
current_humidity = self.current_humidity

View File

@ -10,8 +10,8 @@ from homeassistant.components.climate import (
DOMAIN, ClimateDevice, STATE_AUTO, STATE_COOL, STATE_HEAT,
SUPPORT_TARGET_TEMPERATURE, SUPPORT_FAN_MODE,
SUPPORT_OPERATION_MODE, SUPPORT_SWING_MODE)
from homeassistant.components.zwave import ZWaveDeviceEntity
from homeassistant.components.zwave import async_setup_platform # noqa pylint: disable=unused-import
from homeassistant.components.zwave import ( # noqa pylint: disable=unused-import
ZWaveDeviceEntity, async_setup_platform)
from homeassistant.const import (
STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE)

View File

@ -23,12 +23,17 @@ from homeassistant.components.alexa import smart_home as alexa_sh
from homeassistant.components.google_assistant import helpers as ga_h
from homeassistant.components.google_assistant import const as ga_c
from . import http_api, iot
from . import http_api, iot, auth_api
from .const import CONFIG_DIR, DOMAIN, SERVERS
REQUIREMENTS = ['warrant==0.6.1']
STORAGE_KEY = DOMAIN
STORAGE_VERSION = 1
STORAGE_ENABLE_ALEXA = 'alexa_enabled'
STORAGE_ENABLE_GOOGLE = 'google_enabled'
_LOGGER = logging.getLogger(__name__)
_UNDEF = object()
CONF_ALEXA = 'alexa'
CONF_ALIASES = 'aliases'
@ -39,6 +44,7 @@ CONF_GOOGLE_ACTIONS = 'google_actions'
CONF_RELAYER = 'relayer'
CONF_USER_POOL_ID = 'user_pool_id'
CONF_GOOGLE_ACTIONS_SYNC_URL = 'google_actions_sync_url'
CONF_SUBSCRIPTION_INFO_URL = 'subscription_info_url'
DEFAULT_MODE = 'production'
DEPENDENCIES = ['http']
@ -79,6 +85,7 @@ CONFIG_SCHEMA = vol.Schema({
vol.Optional(CONF_REGION): str,
vol.Optional(CONF_RELAYER): str,
vol.Optional(CONF_GOOGLE_ACTIONS_SYNC_URL): str,
vol.Optional(CONF_SUBSCRIPTION_INFO_URL): str,
vol.Optional(CONF_ALEXA): ALEXA_SCHEMA,
vol.Optional(CONF_GOOGLE_ACTIONS): GACTIONS_SCHEMA,
}),
@ -114,18 +121,21 @@ class Cloud:
def __init__(self, hass, mode, alexa, google_actions,
cognito_client_id=None, user_pool_id=None, region=None,
relayer=None, google_actions_sync_url=None):
relayer=None, google_actions_sync_url=None,
subscription_info_url=None):
"""Create an instance of Cloud."""
self.hass = hass
self.mode = mode
self.alexa_config = alexa
self._google_actions = google_actions
self._gactions_config = None
self._prefs = None
self.jwt_keyset = None
self.id_token = None
self.access_token = None
self.refresh_token = None
self.iot = iot.CloudIoT(self)
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
if mode == MODE_DEV:
self.cognito_client_id = cognito_client_id
@ -133,6 +143,7 @@ class Cloud:
self.region = region
self.relayer = relayer
self.google_actions_sync_url = google_actions_sync_url
self.subscription_info_url = subscription_info_url
else:
info = SERVERS[mode]
@ -142,6 +153,7 @@ class Cloud:
self.region = info['region']
self.relayer = info['relayer']
self.google_actions_sync_url = info['google_actions_sync_url']
self.subscription_info_url = info['subscription_info_url']
@property
def is_logged_in(self):
@ -188,6 +200,16 @@ class Cloud:
return self._gactions_config
@property
def alexa_enabled(self):
"""Return if Alexa is enabled."""
return self._prefs[STORAGE_ENABLE_ALEXA]
@property
def google_enabled(self):
"""Return if Google is enabled."""
return self._prefs[STORAGE_ENABLE_GOOGLE]
def path(self, *parts):
"""Get config path inside cloud dir.
@ -195,6 +217,15 @@ class Cloud:
"""
return self.hass.config.path(CONFIG_DIR, *parts)
async def fetch_subscription_info(self):
"""Fetch subscription info."""
await self.hass.async_add_executor_job(auth_api.check_token, self)
websession = self.hass.helpers.aiohttp_client.async_get_clientsession()
return await websession.get(
self.subscription_info_url, headers={
'authorization': self.id_token
})
@asyncio.coroutine
def logout(self):
"""Close connection and remove all credentials."""
@ -217,10 +248,23 @@ class Cloud:
'refresh_token': self.refresh_token,
}, indent=4))
@asyncio.coroutine
def async_start(self, _):
async def async_start(self, _):
"""Start the cloud component."""
success = yield from self._fetch_jwt_keyset()
prefs = await self._store.async_load()
if prefs is None:
prefs = {}
if self.mode not in prefs:
# Default to True if already logged in to make this not a
# breaking change.
enabled = await self.hass.async_add_executor_job(
os.path.isfile, self.user_info_path)
prefs = {
STORAGE_ENABLE_ALEXA: enabled,
STORAGE_ENABLE_GOOGLE: enabled,
}
self._prefs = prefs
success = await self._fetch_jwt_keyset()
# Fetching keyset can fail if internet is not up yet.
if not success:
@ -241,7 +285,7 @@ class Cloud:
with open(user_info, 'rt') as file:
return json.loads(file.read())
info = yield from self.hass.async_add_job(load_config)
info = await self.hass.async_add_job(load_config)
if info is None:
return
@ -260,6 +304,15 @@ class Cloud:
self.hass.add_job(self.iot.connect())
async def update_preferences(self, *, google_enabled=_UNDEF,
alexa_enabled=_UNDEF):
"""Update user preferences."""
if google_enabled is not _UNDEF:
self._prefs[STORAGE_ENABLE_GOOGLE] = google_enabled
if alexa_enabled is not _UNDEF:
self._prefs[STORAGE_ENABLE_ALEXA] = alexa_enabled
await self._store.async_save(self._prefs)
@asyncio.coroutine
def _fetch_jwt_keyset(self):
"""Fetch the JWT keyset for the Cognito instance."""

View File

@ -11,6 +11,8 @@ SERVERS = {
'relayer': 'wss://cloud.hass.io:8000/websocket',
'google_actions_sync_url': ('https://24ab3v80xd.execute-api.us-east-1.'
'amazonaws.com/prod/smart_home_sync'),
'subscription_info_url': ('https://stripe-api.nabucasa.com/payments/'
'subscription_info')
}
}

View File

@ -6,22 +6,56 @@ import logging
import async_timeout
import voluptuous as vol
from homeassistant.core import callback
from homeassistant.components.http import HomeAssistantView
from homeassistant.components.http.data_validator import (
RequestDataValidator)
from homeassistant.components import websocket_api
from . import auth_api
from .const import DOMAIN, REQUEST_TIMEOUT
from .iot import STATE_DISCONNECTED
_LOGGER = logging.getLogger(__name__)
WS_TYPE_STATUS = 'cloud/status'
SCHEMA_WS_STATUS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_STATUS,
})
WS_TYPE_UPDATE_PREFS = 'cloud/update_prefs'
SCHEMA_WS_UPDATE_PREFS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_UPDATE_PREFS,
vol.Optional('google_enabled'): bool,
vol.Optional('alexa_enabled'): bool,
})
WS_TYPE_SUBSCRIPTION = 'cloud/subscription'
SCHEMA_WS_SUBSCRIPTION = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_SUBSCRIPTION,
})
async def async_setup(hass):
"""Initialize the HTTP API."""
hass.components.websocket_api.async_register_command(
WS_TYPE_STATUS, websocket_cloud_status,
SCHEMA_WS_STATUS
)
hass.components.websocket_api.async_register_command(
WS_TYPE_SUBSCRIPTION, websocket_subscription,
SCHEMA_WS_SUBSCRIPTION
)
hass.components.websocket_api.async_register_command(
WS_TYPE_UPDATE_PREFS, websocket_update_prefs,
SCHEMA_WS_UPDATE_PREFS
)
hass.http.register_view(GoogleActionsSyncView)
hass.http.register_view(CloudLoginView)
hass.http.register_view(CloudLogoutView)
hass.http.register_view(CloudAccountView)
hass.http.register_view(CloudRegisterView)
hass.http.register_view(CloudResendConfirmView)
hass.http.register_view(CloudForgotPasswordView)
@ -102,9 +136,7 @@ class CloudLoginView(HomeAssistantView):
data['password'])
hass.async_add_job(cloud.iot.connect)
# Allow cloud to start connecting.
await asyncio.sleep(0, loop=hass.loop)
return self.json(_account_data(cloud))
return self.json({'success': True})
class CloudLogoutView(HomeAssistantView):
@ -125,23 +157,6 @@ class CloudLogoutView(HomeAssistantView):
return self.json_message('ok')
class CloudAccountView(HomeAssistantView):
"""View to retrieve account info."""
url = '/api/cloud/account'
name = 'api:cloud:account'
async def get(self, request):
"""Get account info."""
hass = request.app['hass']
cloud = hass.data[DOMAIN]
if not cloud.is_logged_in:
return self.json_message('Not logged in', 400)
return self.json(_account_data(cloud))
class CloudRegisterView(HomeAssistantView):
"""Register on the Home Assistant cloud."""
@ -209,12 +224,73 @@ class CloudForgotPasswordView(HomeAssistantView):
return self.json_message('ok')
@callback
def websocket_cloud_status(hass, connection, msg):
"""Handle request for account info.
Async friendly.
"""
cloud = hass.data[DOMAIN]
connection.to_write.put_nowait(
websocket_api.result_message(msg['id'], _account_data(cloud)))
@websocket_api.async_response
async def websocket_subscription(hass, connection, msg):
"""Handle request for account info."""
cloud = hass.data[DOMAIN]
if not cloud.is_logged_in:
connection.to_write.put_nowait(websocket_api.error_message(
msg['id'], 'not_logged_in',
'You need to be logged in to the cloud.'))
return
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
response = await cloud.fetch_subscription_info()
if response.status == 200:
connection.send_message_outside(websocket_api.result_message(
msg['id'], await response.json()))
else:
connection.send_message_outside(websocket_api.error_message(
msg['id'], 'request_failed', 'Failed to request subscription'))
@websocket_api.async_response
async def websocket_update_prefs(hass, connection, msg):
"""Handle request for account info."""
cloud = hass.data[DOMAIN]
if not cloud.is_logged_in:
connection.to_write.put_nowait(websocket_api.error_message(
msg['id'], 'not_logged_in',
'You need to be logged in to the cloud.'))
return
changes = dict(msg)
changes.pop('id')
changes.pop('type')
await cloud.update_preferences(**changes)
connection.send_message_outside(websocket_api.result_message(
msg['id'], {'success': True}))
def _account_data(cloud):
"""Generate the auth data JSON response."""
if not cloud.is_logged_in:
return {
'logged_in': False,
'cloud': STATE_DISCONNECTED,
}
claims = cloud.claims
return {
'logged_in': True,
'email': claims['email'],
'sub_exp': claims['custom:sub-exp'],
'cloud': cloud.iot.state,
'google_enabled': cloud.google_enabled,
'alexa_enabled': cloud.alexa_enabled,
}

View File

@ -227,6 +227,9 @@ def async_handle_message(hass, cloud, handler_name, payload):
@asyncio.coroutine
def async_handle_alexa(hass, cloud, payload):
"""Handle an incoming IoT message for Alexa."""
if not cloud.alexa_enabled:
return alexa.turned_off_response(payload)
result = yield from alexa.async_handle_message(
hass, cloud.alexa_config, payload)
return result
@ -236,6 +239,9 @@ def async_handle_alexa(hass, cloud, payload):
@asyncio.coroutine
def async_handle_google_actions(hass, cloud, payload):
"""Handle an incoming IoT message for Google Actions."""
if not cloud.google_enabled:
return ga.turned_off_response(payload)
result = yield from ga.async_handle_message(
hass, cloud.gactions_config, payload)
return result

View File

@ -13,8 +13,17 @@ from homeassistant.util.yaml import load_yaml, dump
DOMAIN = 'config'
DEPENDENCIES = ['http']
SECTIONS = ('core', 'customize', 'group', 'hassbian', 'automation', 'script',
'entity_registry', 'config_entries')
SECTIONS = (
'automation',
'config_entries',
'core',
'customize',
'device_registry',
'entity_registry',
'group',
'hassbian',
'script',
)
ON_DEMAND = ('zwave',)

View File

@ -54,6 +54,7 @@ class ConfigManagerEntryIndexView(HomeAssistantView):
'title': entry.title,
'source': entry.source,
'state': entry.state,
'connection_class': entry.connection_class,
} for entry in hass.config_entries.async_entries()])

View File

@ -0,0 +1,47 @@
"""HTTP views to interact with the device registry."""
import voluptuous as vol
from homeassistant.core import callback
from homeassistant.helpers.device_registry import async_get_registry
from homeassistant.components import websocket_api
DEPENDENCIES = ['websocket_api']
WS_TYPE_LIST = 'config/device_registry/list'
SCHEMA_WS_LIST = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_LIST,
})
async def async_setup(hass):
"""Enable the Entity Registry views."""
hass.components.websocket_api.async_register_command(
WS_TYPE_LIST, websocket_list_devices,
SCHEMA_WS_LIST
)
return True
@callback
def websocket_list_devices(hass, connection, msg):
"""Handle list devices command.
Async friendly.
"""
async def retrieve_entities():
"""Get devices from registry."""
registry = await async_get_registry(hass)
connection.send_message_outside(websocket_api.result_message(
msg['id'], [{
'config_entries': list(entry.config_entries),
'connections': list(entry.connections),
'manufacturer': entry.manufacturer,
'model': entry.model,
'name': entry.name,
'sw_version': entry.sw_version,
'id': entry.id,
'hub_device_id': entry.hub_device_id,
} for entry in registry.devices.values()]
))
hass.async_add_job(retrieve_entities())

View File

@ -8,6 +8,11 @@ from homeassistant.helpers import config_validation as cv
DEPENDENCIES = ['websocket_api']
WS_TYPE_LIST = 'config/entity_registry/list'
SCHEMA_WS_LIST = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_LIST,
})
WS_TYPE_GET = 'config/entity_registry/get'
SCHEMA_WS_GET = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_GET,
@ -26,6 +31,10 @@ SCHEMA_WS_UPDATE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
async def async_setup(hass):
"""Enable the Entity Registry views."""
hass.components.websocket_api.async_register_command(
WS_TYPE_LIST, websocket_list_entities,
SCHEMA_WS_LIST
)
hass.components.websocket_api.async_register_command(
WS_TYPE_GET, websocket_get_entity,
SCHEMA_WS_GET
@ -37,6 +46,29 @@ async def async_setup(hass):
return True
@callback
def websocket_list_entities(hass, connection, msg):
"""Handle list registry entries command.
Async friendly.
"""
async def retrieve_entities():
"""Get entities from registry."""
registry = await async_get_registry(hass)
connection.send_message_outside(websocket_api.result_message(
msg['id'], [{
'config_entry_id': entry.config_entry_id,
'device_id': entry.device_id,
'disabled_by': entry.disabled_by,
'entity_id': entry.entity_id,
'name': entry.name,
'platform': entry.platform,
} for entry in registry.entities.values()]
))
hass.async_add_job(retrieve_entities())
@callback
def websocket_get_entity(hass, connection, msg):
"""Handle get entity registry entry command.

View File

@ -35,8 +35,9 @@ ENTITY_ID_ALL_COVERS = group.ENTITY_ID_FORMAT.format('all_covers')
ENTITY_ID_FORMAT = DOMAIN + '.{}'
DEVICE_CLASSES = [
'window', # Window control
'damper',
'garage', # Garage door control
'window', # Window control
]
DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES))
@ -140,7 +141,7 @@ def stop_cover_tilt(hass, entity_id=None):
async def async_setup(hass, config):
"""Track states and offer events for covers."""
component = EntityComponent(
component = hass.data[DOMAIN] = EntityComponent(
_LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_COVERS)
await component.async_setup(config)
@ -195,6 +196,16 @@ async def async_setup(hass, config):
return True
async def async_setup_entry(hass, entry):
"""Set up a config entry."""
return await hass.data[DOMAIN].async_setup_entry(entry)
async def async_unload_entry(hass, entry):
"""Unload a config entry."""
return await hass.data[DOMAIN].async_unload_entry(entry)
class CoverDevice(Entity):
"""Representation a cover."""

View File

@ -0,0 +1,146 @@
"""
Support for deCONZ covers.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/cover.deconz/
"""
from homeassistant.components.deconz.const import (
COVER_TYPES, DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, DATA_DECONZ_UNSUB,
DECONZ_DOMAIN)
from homeassistant.components.cover import (
ATTR_POSITION, CoverDevice, SUPPORT_CLOSE, SUPPORT_OPEN,
SUPPORT_SET_POSITION)
from homeassistant.core import callback
from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE
from homeassistant.helpers.dispatcher import async_dispatcher_connect
DEPENDENCIES = ['deconz']
async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Unsupported way of setting up deCONZ covers."""
pass
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up covers for deCONZ component.
Covers are based on same device class as lights in deCONZ.
"""
@callback
def async_add_cover(lights):
"""Add cover from deCONZ."""
entities = []
for light in lights:
if light.type in COVER_TYPES:
entities.append(DeconzCover(light))
async_add_entities(entities, True)
hass.data[DATA_DECONZ_UNSUB].append(
async_dispatcher_connect(hass, 'deconz_new_light', async_add_cover))
async_add_cover(hass.data[DATA_DECONZ].lights.values())
class DeconzCover(CoverDevice):
"""Representation of a deCONZ cover."""
def __init__(self, cover):
"""Set up cover and add update callback to get data from websocket."""
self._cover = cover
self._features = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION
async def async_added_to_hass(self):
"""Subscribe to covers events."""
self._cover.register_async_callback(self.async_update_callback)
self.hass.data[DATA_DECONZ_ID][self.entity_id] = self._cover.deconz_id
async def async_will_remove_from_hass(self) -> None:
"""Disconnect cover object when removed."""
self._cover.remove_callback(self.async_update_callback)
self._cover = None
@callback
def async_update_callback(self, reason):
"""Update the cover's state."""
self.async_schedule_update_ha_state()
@property
def current_cover_position(self):
"""Return the current position of the cover."""
if self.is_closed:
return 0
return int(self._cover.brightness / 255 * 100)
@property
def is_closed(self):
"""Return if the cover is closed."""
return not self._cover.state
@property
def name(self):
"""Return the name of the cover."""
return self._cover.name
@property
def unique_id(self):
"""Return a unique identifier for this cover."""
return self._cover.uniqueid
@property
def device_class(self):
"""Return the class of the cover."""
return 'damper'
@property
def supported_features(self):
"""Flag supported features."""
return self._features
@property
def available(self):
"""Return True if light is available."""
return self._cover.reachable
@property
def should_poll(self):
"""No polling needed."""
return False
async def async_set_cover_position(self, **kwargs):
"""Move the cover to a specific position."""
position = kwargs[ATTR_POSITION]
data = {'on': False}
if position > 0:
data['on'] = True
data['bri'] = int(position / 100 * 255)
await self._cover.async_set_state(data)
async def async_open_cover(self, **kwargs):
"""Open cover."""
data = {ATTR_POSITION: 100}
await self.async_set_cover_position(**data)
async def async_close_cover(self, **kwargs):
"""Close cover."""
data = {ATTR_POSITION: 0}
await self.async_set_cover_position(**data)
@property
def device_info(self):
"""Return a device description for device registry."""
if (self._cover.uniqueid is None or
self._cover.uniqueid.count(':') != 7):
return None
serial = self._cover.uniqueid.split('-', 1)[0]
bridgeid = self.hass.data[DATA_DECONZ].config.bridgeid
return {
'connections': {(CONNECTION_ZIGBEE, serial)},
'identifiers': {(DECONZ_DOMAIN, serial)},
'manufacturer': self._cover.manufacturer,
'model': self._cover.modelid,
'name': self._cover.name,
'sw_version': self._cover.swversion,
'via_hub': (DECONZ_DOMAIN, bridgeid),
}

View File

@ -44,6 +44,8 @@ class ISYCoverDevice(ISYDevice, CoverDevice):
@property
def current_cover_position(self) -> int:
"""Return the current cover position."""
if self.is_unknown() or self.value is None:
return None
return sorted((0, self.value, 100))[1]
@property

View File

@ -11,8 +11,8 @@ import voluptuous as vol
from homeassistant.components.cover import (
CoverDevice, SUPPORT_CLOSE, SUPPORT_OPEN)
from homeassistant.const import (
CONF_PASSWORD, CONF_TYPE, CONF_USERNAME, STATE_CLOSED, STATE_CLOSING,
STATE_OPENING)
CONF_PASSWORD, CONF_TYPE, CONF_USERNAME, STATE_CLOSED, STATE_OPEN,
STATE_CLOSING, STATE_OPENING)
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pymyq==0.0.15']
@ -23,6 +23,7 @@ DEFAULT_NAME = 'myq'
MYQ_TO_HASS = {
'closed': STATE_CLOSED,
'open': STATE_OPEN,
'closing': STATE_CLOSING,
'opening': STATE_OPENING
}

View File

@ -45,6 +45,11 @@ class TuyaCover(TuyaDevice, CoverDevice):
@property
def is_closed(self):
"""Return if the cover is closed or not."""
state = self.tuya.state()
if state == 1:
return False
if state == 2:
return True
return None
def open_cover(self, **kwargs):

View File

@ -9,10 +9,9 @@ https://home-assistant.io/components/cover.zwave/
import logging
from homeassistant.components.cover import (
DOMAIN, SUPPORT_OPEN, SUPPORT_CLOSE, ATTR_POSITION)
from homeassistant.components.zwave import ZWaveDeviceEntity
from homeassistant.components import zwave
from homeassistant.components.zwave import async_setup_platform # noqa pylint: disable=unused-import
from homeassistant.components.zwave import workaround
from homeassistant.components.zwave import ( # noqa pylint: disable=unused-import
ZWaveDeviceEntity, async_setup_platform, workaround)
from homeassistant.components.cover import CoverDevice
_LOGGER = logging.getLogger(__name__)

View File

@ -28,6 +28,6 @@
"title": "Opcions de configuraci\u00f3 addicionals per deCONZ"
}
},
"title": "deCONZ"
"title": "Passarel\u00b7la d'enlla\u00e7 deCONZ Zigbee"
}
}

View File

@ -14,10 +14,10 @@
"host": "Host",
"port": "Port (Standartwert : '80')"
},
"title": "Definieren Sie den deCONZ-Gateway"
"title": "Definiere das deCONZ-Gateway"
},
"link": {
"description": "Entsperren Sie Ihr deCONZ-Gateway, um sich bei Home Assistant zu registrieren. \n\n 1. Gehen Sie zu den deCONZ-Systemeinstellungen \n 2. Dr\u00fccken Sie die Taste \"Gateway entsperren\"",
"description": "Entsperre dein deCONZ-Gateway, um dich bei Home Assistant zu registrieren. \n\n 1. Gehe zu den deCONZ-Systemeinstellungen \n 2. Dr\u00fccke die Taste \"Gateway entsperren\"",
"title": "Mit deCONZ verbinden"
},
"options": {

View File

@ -2,7 +2,8 @@
"config": {
"abort": {
"already_configured": "\u05d4\u05de\u05d2\u05e9\u05e8 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8",
"no_bridges": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05d2\u05e9\u05e8\u05d9 deCONZ"
"no_bridges": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05d2\u05e9\u05e8\u05d9 deCONZ",
"one_instance_only": "\u05d4\u05e8\u05db\u05d9\u05d1 \u05ea\u05d5\u05de\u05da \u05e8\u05e7 \u05d0\u05d7\u05d3 deCONZ \u05dc\u05de\u05e9\u05dc"
},
"error": {
"no_key": "\u05dc\u05d0 \u05e0\u05d9\u05ea\u05df \u05d4\u05d9\u05d4 \u05dc\u05e7\u05d1\u05dc \u05de\u05e4\u05ea\u05d7 API"

View File

@ -0,0 +1,33 @@
{
"config": {
"abort": {
"already_configured": "Bridge sudah dikonfigurasi",
"no_bridges": "deCONZ bridges tidak ditemukan",
"one_instance_only": "Komponen hanya mendukung satu instance deCONZ"
},
"error": {
"no_key": "Tidak bisa mendapatkan kunci API"
},
"step": {
"init": {
"data": {
"host": "Host",
"port": "Port (nilai default: '80')"
},
"title": "Tentukan deCONZ gateway"
},
"link": {
"description": "Buka gerbang deCONZ Anda untuk mendaftar dengan Home Assistant. \n\n 1. Pergi ke pengaturan sistem deCONZ \n 2. Tekan tombol \"Buka Kunci Gateway\"",
"title": "Tautan dengan deCONZ"
},
"options": {
"data": {
"allow_clip_sensor": "Izinkan mengimpor sensor virtual",
"allow_deconz_groups": "Izinkan mengimpor grup deCONZ"
},
"title": "Opsi konfigurasi tambahan untuk deCONZ"
}
},
"title": "deCONZ Zigbee gateway"
}
}

View File

@ -3,7 +3,7 @@
"abort": {
"already_configured": "\ube0c\ub9bf\uc9c0\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"no_bridges": "\ubc1c\uacac\ub41c deCONZ \ube0c\ub9bf\uc9c0\uac00 \uc5c6\uc2b5\ub2c8\ub2e4",
"one_instance_only": "\uad6c\uc131\uc694\uc18c\ub294 \ud558\ub098\uc758 deCONZ \uc778\uc2a4\ud134\uc2a4 \ub9cc \uc9c0\uc6d0\ud569\ub2c8\ub2e4"
"one_instance_only": "\uad6c\uc131\uc694\uc18c\ub294 \ud558\ub098\uc758 deCONZ \uc778\uc2a4\ud134\uc2a4\ub9cc \uc9c0\uc6d0\ud569\ub2c8\ub2e4"
},
"error": {
"no_key": "API \ud0a4\ub97c \uac00\uc838\uc62c \uc218 \uc5c6\uc2b5\ub2c8\ub2e4"

View File

@ -0,0 +1,33 @@
{
"config": {
"abort": {
"already_configured": "Brua er allereie konfigurert",
"no_bridges": "Oppdaga ingen deCONZ-bruer",
"one_instance_only": "Komponenten st\u00f8ttar berre \u00e9in deCONZ-instans"
},
"error": {
"no_key": "Kunne ikkje f\u00e5 ein API-n\u00f8kkel"
},
"step": {
"init": {
"data": {
"host": "Vert",
"port": "Port (standardverdi: '80')"
},
"title": "Definer deCONZ-gateway"
},
"link": {
"description": "L\u00e5s opp deCONZ-gatewayen din for \u00e5 registrere den med Home Assistant.\n\n1. G\u00e5 til systeminnstillingane til deCONZ\n2. Trykk p\u00e5 \"L\u00e5s opp gateway\"-knappen",
"title": "Link med deCONZ"
},
"options": {
"data": {
"allow_clip_sensor": "Tillat importering av virtuelle sensorar",
"allow_deconz_groups": "Tillat importering av deCONZ-grupper"
},
"title": "Ekstra konfigurasjonsalternativ for deCONZ"
}
},
"title": "deCONZ Zigbee gateway"
}
}

View File

@ -26,6 +26,9 @@ from .const import (
REQUIREMENTS = ['pydeconz==47']
SUPPORTED_PLATFORMS = ['binary_sensor', 'cover',
'light', 'scene', 'sensor', 'switch']
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Optional(CONF_API_KEY): cv.string,
@ -104,7 +107,7 @@ async def async_setup_entry(hass, config_entry):
hass.data[DATA_DECONZ_EVENT] = []
hass.data[DATA_DECONZ_UNSUB] = []
for component in ['binary_sensor', 'light', 'scene', 'sensor', 'switch']:
for component in SUPPORTED_PLATFORMS:
hass.async_create_task(hass.config_entries.async_forward_entry_setup(
config_entry, component))
@ -127,7 +130,7 @@ async def async_setup_entry(hass, config_entry):
device_registry = await \
hass.helpers.device_registry.async_get_registry()
device_registry.async_get_or_create(
config_entry=config_entry.entry_id,
config_entry_id=config_entry.entry_id,
connections={(CONNECTION_NETWORK_MAC, deconz.config.mac)},
identifiers={(DOMAIN, deconz.config.bridgeid)},
manufacturer='Dresden Elektronik', model=deconz.config.modelid,
@ -228,7 +231,7 @@ async def async_unload_entry(hass, config_entry):
hass.services.async_remove(DOMAIN, SERVICE_DECONZ)
deconz.close()
for component in ['binary_sensor', 'light', 'scene', 'sensor', 'switch']:
for component in SUPPORTED_PLATFORMS:
await hass.config_entries.async_forward_entry_unload(
config_entry, component)

View File

@ -2,7 +2,7 @@
import voluptuous as vol
from homeassistant import config_entries, data_entry_flow
from homeassistant import config_entries
from homeassistant.core import callback
from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT
from homeassistant.helpers import aiohttp_client
@ -23,10 +23,11 @@ def configured_hosts(hass):
@config_entries.HANDLERS.register(DOMAIN)
class DeconzFlowHandler(data_entry_flow.FlowHandler):
class DeconzFlowHandler(config_entries.ConfigFlow):
"""Handle a deCONZ config flow."""
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH
def __init__(self):
"""Initialize the deCONZ config flow."""

View File

@ -16,6 +16,8 @@ CONF_ALLOW_DECONZ_GROUPS = 'allow_deconz_groups'
ATTR_DARK = 'dark'
ATTR_ON = 'on'
COVER_TYPES = ["Level controllable output"]
POWER_PLUGS = ["On/Off plug-in unit", "Smart plug"]
SIRENS = ["Warning device"]
SWITCH_TYPES = POWER_PLUGS + SIRENS

View File

@ -5,19 +5,30 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.bbox/
"""
from collections import namedtuple
import logging
from datetime import timedelta
import logging
import homeassistant.util.dt as dt_util
from homeassistant.components.device_tracker import DOMAIN, DeviceScanner
import voluptuous as vol
from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST
import homeassistant.helpers.config_validation as cv
from homeassistant.util import Throttle
import homeassistant.util.dt as dt_util
REQUIREMENTS = ['pybbox==0.0.5-alpha']
_LOGGER = logging.getLogger(__name__)
DEFAULT_HOST = '192.168.1.254'
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=60)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
})
def get_scanner(hass, config):
"""Validate the configuration and return a Bbox scanner."""
@ -33,6 +44,9 @@ class BboxDeviceScanner(DeviceScanner):
"""This class scans for devices connected to the bbox."""
def __init__(self, config):
"""Get host from config."""
self.host = config[CONF_HOST]
"""Initialize the scanner."""
self.last_results = [] # type: List[Device]
@ -64,7 +78,7 @@ class BboxDeviceScanner(DeviceScanner):
import pybbox
box = pybbox.Bbox()
box = pybbox.Bbox(ip=self.host)
result = box.get_all_connected_devices()
now = dt_util.now()

View File

@ -0,0 +1,65 @@
"""
Support for Huawei LTE routers.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.huawei_lte/
"""
from typing import Any, Dict, List, Optional
import attr
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import (
PLATFORM_SCHEMA, DeviceScanner,
)
from homeassistant.const import CONF_URL
from ..huawei_lte import DATA_KEY, RouterData
DEPENDENCIES = ['huawei_lte']
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_URL): cv.url,
})
def get_scanner(hass, config):
"""Get a Huawei LTE router scanner."""
data = hass.data[DATA_KEY].get_data(config)
return HuaweiLteScanner(data)
@attr.s
class HuaweiLteScanner(DeviceScanner):
"""Huawei LTE router scanner."""
data = attr.ib(type=RouterData)
_hosts = attr.ib(init=False, factory=dict)
def scan_devices(self) -> List[str]:
"""Scan for devices."""
self.data.update()
self._hosts = {
x["MacAddress"]: x
for x in self.data["wlan_host_list.Hosts.Host"]
if x.get("MacAddress")
}
return list(self._hosts)
def get_device_name(self, device: str) -> Optional[str]:
"""Get name for a device."""
host = self._hosts.get(device)
return host.get("HostName") or None if host else None
def get_extra_attributes(self, device: str) -> Dict[str, Any]:
"""
Get extra attributes of a device.
Some known extra attributes that may be returned in the dict
include MacAddress (MAC address), ID (client ID), IpAddress
(IP address), AssociatedSsid (associated SSID), AssociatedTime
(associated time in seconds), and HostName (host name).
"""
return self._hosts.get(device) or {}

View File

@ -14,7 +14,7 @@ from homeassistant.components.device_tracker import (
from homeassistant.const import (
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT)
REQUIREMENTS = ['librouteros==2.1.0']
REQUIREMENTS = ['librouteros==2.1.1']
MTK_DEFAULT_API_PORT = '8728'

View File

@ -48,6 +48,7 @@ CONFIG_ENTRY_HANDLERS = {
SERVICE_DECONZ: 'deconz',
'google_cast': 'cast',
SERVICE_HUE: 'hue',
SERVICE_IKEA_TRADFRI: 'tradfri',
'sonos': 'sonos',
}
@ -55,7 +56,6 @@ SERVICE_HANDLERS = {
SERVICE_HASS_IOS_APP: ('ios', None),
SERVICE_NETGEAR: ('device_tracker', None),
SERVICE_WEMO: ('wemo', None),
SERVICE_IKEA_TRADFRI: ('tradfri', None),
SERVICE_HASSIO: ('hassio', None),
SERVICE_AXIS: ('axis', None),
SERVICE_APPLE_TV: ('apple_tv', None),

View File

@ -15,7 +15,7 @@ from homeassistant.helpers import discovery
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, \
EVENT_HOMEASSISTANT_STOP
REQUIREMENTS = ['sucks==0.9.1']
REQUIREMENTS = ['sucks==0.9.3']
_LOGGER = logging.getLogger(__name__)
@ -59,8 +59,9 @@ def setup(hass, config):
_LOGGER.debug("Ecobot devices: %s", devices)
for device in devices:
_LOGGER.info("Discovered Ecovacs device on account: %s",
device['nick'])
_LOGGER.info(
"Discovered Ecovacs device on account: %s with nickname %s",
device['did'], device['nick'])
vacbot = VacBot(ecovacs_api.uid,
ecovacs_api.REALM,
ecovacs_api.resource,
@ -74,7 +75,7 @@ def setup(hass, config):
"""Shut down open connections to Ecovacs XMPP server."""
for device in hass.data[ECOVACS_DEVICES]:
_LOGGER.info("Shutting down connection to Ecovacs device %s",
device.vacuum['nick'])
device.vacuum['did'])
device.disconnect()
# Listen for HA stop to disconnect.

View File

@ -0,0 +1,135 @@
"""
Support for EDP re:dy.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/edp_redy/
"""
import logging
from datetime import timedelta
import voluptuous as vol
from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD,
EVENT_HOMEASSISTANT_START)
from homeassistant.core import callback
from homeassistant.helpers import discovery, dispatcher, aiohttp_client
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import async_track_point_in_time
from homeassistant.util import dt as dt_util
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'edp_redy'
EDP_REDY = 'edp_redy'
DATA_UPDATE_TOPIC = '{0}_data_update'.format(DOMAIN)
UPDATE_INTERVAL = 30
REQUIREMENTS = ['edp_redy==0.0.2']
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string
})
}, extra=vol.ALLOW_EXTRA)
async def async_setup(hass, config):
"""Set up the EDP re:dy component."""
from edp_redy import EdpRedySession
session = EdpRedySession(config[DOMAIN][CONF_USERNAME],
config[DOMAIN][CONF_PASSWORD],
aiohttp_client.async_get_clientsession(hass),
hass.loop)
hass.data[EDP_REDY] = session
platform_loaded = False
async def async_update_and_sched(time):
update_success = await session.async_update()
if update_success:
nonlocal platform_loaded
# pylint: disable=used-before-assignment
if not platform_loaded:
for component in ['sensor', 'switch']:
await discovery.async_load_platform(hass, component,
DOMAIN, {}, config)
platform_loaded = True
dispatcher.async_dispatcher_send(hass, DATA_UPDATE_TOPIC)
# schedule next update
async_track_point_in_time(hass, async_update_and_sched,
time + timedelta(seconds=UPDATE_INTERVAL))
async def start_component(event):
_LOGGER.debug("Starting updates")
await async_update_and_sched(dt_util.utcnow())
# only start fetching data after HA boots to prevent delaying the boot
# process
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_component)
return True
class EdpRedyDevice(Entity):
"""Representation a base re:dy device."""
def __init__(self, session, device_id, name):
"""Initialize the device."""
self._session = session
self._state = None
self._is_available = True
self._device_state_attributes = {}
self._id = device_id
self._unique_id = device_id
self._name = name if name else device_id
async def async_added_to_hass(self):
"""Subscribe to the data updates topic."""
dispatcher.async_dispatcher_connect(
self.hass, DATA_UPDATE_TOPIC, self._data_updated)
@property
def name(self):
"""Return the name of the device."""
return self._name
@property
def unique_id(self) -> str:
"""Return a unique ID."""
return self._unique_id
@property
def available(self):
"""Return True if entity is available."""
return self._is_available
@property
def should_poll(self):
"""Return the polling state. No polling needed."""
return False
@property
def device_state_attributes(self):
"""Return the state attributes."""
return self._device_state_attributes
@callback
def _data_updated(self):
"""Update state, trigger updates."""
self.async_schedule_update_ha_state(True)
def _parse_data(self, data):
"""Parse data received from the server."""
if "OutOfOrder" in data:
try:
self._is_available = not data['OutOfOrder']
except ValueError:
_LOGGER.error(
"Could not parse OutOfOrder for %s", self._id)
self._is_available = False

View File

@ -4,7 +4,6 @@ Fans on Zigbee Home Automation networks.
For more details on this platform, please refer to the documentation
at https://home-assistant.io/components/fan.zha/
"""
import asyncio
import logging
from homeassistant.components import zha
from homeassistant.components.fan import (
@ -38,9 +37,8 @@ VALUE_TO_SPEED = {i: speed for i, speed in enumerate(SPEED_LIST)}
SPEED_TO_VALUE = {speed: i for i, speed in enumerate(SPEED_LIST)}
@asyncio.coroutine
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 Zigbee Home Automation fans."""
discovery_info = zha.get_discovery_info(hass, discovery_info)
if discovery_info is None:
@ -76,32 +74,36 @@ class ZhaFan(zha.Entity, FanEntity):
return False
return self._state != SPEED_OFF
@asyncio.coroutine
def async_turn_on(self, speed: str = None, **kwargs) -> None:
async def async_turn_on(self, speed: str = None, **kwargs) -> None:
"""Turn the entity on."""
if speed is None:
speed = SPEED_MEDIUM
yield from self.async_set_speed(speed)
await self.async_set_speed(speed)
@asyncio.coroutine
def async_turn_off(self, **kwargs) -> None:
async def async_turn_off(self, **kwargs) -> None:
"""Turn the entity off."""
yield from self.async_set_speed(SPEED_OFF)
await self.async_set_speed(SPEED_OFF)
@asyncio.coroutine
def async_set_speed(self, speed: str) -> None:
async def async_set_speed(self, speed: str) -> None:
"""Set the speed of the fan."""
yield from self._endpoint.fan.write_attributes({
'fan_mode': SPEED_TO_VALUE[speed]})
from zigpy.exceptions import DeliveryError
try:
await self._endpoint.fan.write_attributes(
{'fan_mode': SPEED_TO_VALUE[speed]}
)
except DeliveryError as ex:
_LOGGER.error("%s: Could not set speed: %s", self.entity_id, ex)
return
self._state = speed
self.async_schedule_update_ha_state()
@asyncio.coroutine
def async_update(self):
async def async_update(self):
"""Retrieve latest state."""
result = yield from zha.safe_read(self._endpoint.fan, ['fan_mode'])
result = await zha.safe_read(self._endpoint.fan, ['fan_mode'],
allow_cache=False,
only_cache=(not self._initialized))
new_value = result.get('fan_mode', None)
self._state = VALUE_TO_SPEED.get(new_value, None)

View File

@ -26,7 +26,7 @@ from homeassistant.helpers.translation import async_get_translations
from homeassistant.loader import bind_hass
from homeassistant.util.yaml import load_yaml
REQUIREMENTS = ['home-assistant-frontend==20180916.0']
REQUIREMENTS = ['home-assistant-frontend==20180927.0']
DOMAIN = 'frontend'
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log',

View File

@ -14,6 +14,7 @@ from typing import Optional
from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
_LOGGER = logging.getLogger(__name__)

View File

@ -8,7 +8,6 @@ import logging
import random
from datetime import timedelta
from math import pi, cos, sin, radians
from typing import Optional
from homeassistant.components.geo_location import GeoLocationEvent

View File

@ -0,0 +1,196 @@
"""
Generic GeoJSON events platform.
Retrieves current events (typically incidents or alerts) in GeoJSON format, and
displays information on events filtered by distance to the HA instance's
location.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/geo_location/geo_json_events/
"""
import logging
from datetime import timedelta
from typing import Optional
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.geo_location import GeoLocationEvent
from homeassistant.const import CONF_RADIUS, CONF_URL, CONF_SCAN_INTERVAL, \
EVENT_HOMEASSISTANT_START
from homeassistant.components.geo_location import PLATFORM_SCHEMA
from homeassistant.helpers.event import track_time_interval
REQUIREMENTS = ['geojson_client==0.1']
_LOGGER = logging.getLogger(__name__)
ATTR_EXTERNAL_ID = 'external_id'
DEFAULT_RADIUS_IN_KM = 20.0
DEFAULT_UNIT_OF_MEASUREMENT = "km"
SCAN_INTERVAL = timedelta(minutes=5)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_URL): cv.string,
vol.Optional(CONF_RADIUS, default=DEFAULT_RADIUS_IN_KM):
vol.Coerce(float),
})
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the GeoJSON Events platform."""
url = config[CONF_URL]
scan_interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL)
radius_in_km = config[CONF_RADIUS]
# Initialize the entity manager.
GeoJsonFeedManager(hass, add_entities, scan_interval, url, radius_in_km)
class GeoJsonFeedManager:
"""Feed Manager for GeoJSON feeds."""
def __init__(self, hass, add_entities, scan_interval, url, radius_in_km):
"""Initialize the GeoJSON Feed Manager."""
from geojson_client.generic_feed import GenericFeed
self._hass = hass
self._feed = GenericFeed((hass.config.latitude, hass.config.longitude),
filter_radius=radius_in_km, url=url)
self._add_entities = add_entities
self._scan_interval = scan_interval
self._feed_entries = []
self._managed_entities = []
hass.bus.listen_once(
EVENT_HOMEASSISTANT_START, lambda _: self._update())
self._init_regular_updates()
def _init_regular_updates(self):
"""Schedule regular updates at the specified interval."""
track_time_interval(self._hass, lambda now: self._update(),
self._scan_interval)
def _update(self):
"""Update the feed and then update connected entities."""
import geojson_client
status, feed_entries = self._feed.update()
if status == geojson_client.UPDATE_OK:
_LOGGER.debug("Data retrieved %s", feed_entries)
# Keep a copy of all feed entries for future lookups by entities.
self._feed_entries = feed_entries.copy()
keep_entries = self._update_or_remove_entities(feed_entries)
self._generate_new_entities(keep_entries)
elif status == geojson_client.UPDATE_OK_NO_DATA:
_LOGGER.debug("Update successful, but no data received from %s",
self._feed)
else:
_LOGGER.warning("Update not successful, no data received from %s",
self._feed)
# Remove all entities.
self._update_or_remove_entities([])
def _update_or_remove_entities(self, feed_entries):
"""Update existing entries and remove obsolete entities."""
_LOGGER.debug("Entries for updating: %s", feed_entries)
remove_entry = None
# Remove obsolete entities for events that have disappeared
managed_entities = self._managed_entities.copy()
for entity in managed_entities:
# Remove entry from previous iteration - if applicable.
if remove_entry:
feed_entries.remove(remove_entry)
remove_entry = None
for entry in feed_entries:
if entity.external_id == entry.external_id:
# Existing entity - update details.
_LOGGER.debug("Existing entity found %s", entity)
remove_entry = entry
entity.schedule_update_ha_state(True)
break
else:
# Remove obsolete entity.
_LOGGER.debug("Entity not current anymore %s", entity)
self._managed_entities.remove(entity)
self._hass.add_job(entity.async_remove())
# Remove entry from very last iteration - if applicable.
if remove_entry:
feed_entries.remove(remove_entry)
# Return the remaining entries that new entities must be created for.
return feed_entries
def _generate_new_entities(self, entries):
"""Generate new entities for events."""
new_entities = []
for entry in entries:
new_entity = GeoJsonLocationEvent(self, entry)
_LOGGER.debug("New entity added %s", new_entity)
new_entities.append(new_entity)
# Add new entities to HA and keep track of them in this manager.
self._add_entities(new_entities, True)
self._managed_entities.extend(new_entities)
def get_feed_entry(self, external_id):
"""Return a feed entry identified by external id."""
return next((entry for entry in self._feed_entries
if entry.external_id == external_id), None)
class GeoJsonLocationEvent(GeoLocationEvent):
"""This represents an external event with GeoJSON data."""
def __init__(self, feed_manager, feed_entry):
"""Initialize entity with data from feed entry."""
self._feed_manager = feed_manager
self._update_from_feed(feed_entry)
@property
def should_poll(self):
"""No polling needed for GeoJSON location events."""
return False
async def async_update(self):
"""Update this entity from the data held in the feed manager."""
feed_entry = self._feed_manager.get_feed_entry(self.external_id)
if feed_entry:
self._update_from_feed(feed_entry)
def _update_from_feed(self, feed_entry):
"""Update the internal state from the provided feed entry."""
self._name = feed_entry.title
self._distance = feed_entry.distance_to_home
self._latitude = feed_entry.coordinates[0]
self._longitude = feed_entry.coordinates[1]
self.external_id = feed_entry.external_id
@property
def name(self) -> Optional[str]:
"""Return the name of the entity."""
return self._name
@property
def distance(self) -> Optional[float]:
"""Return distance value of this external event."""
return self._distance
@property
def latitude(self) -> Optional[float]:
"""Return latitude value of this external event."""
return self._latitude
@property
def longitude(self) -> Optional[float]:
"""Return longitude value of this external event."""
return self._longitude
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return DEFAULT_UNIT_OF_MEASUREMENT
@property
def device_state_attributes(self):
"""Return the device state attributes."""
attributes = {}
if self.external_id:
attributes[ATTR_EXTERNAL_ID] = self.external_id
return attributes

View File

@ -6,6 +6,7 @@ https://home-assistant.io/components/google_assistant/
"""
import asyncio
import logging
from typing import Dict, Any
import aiohttp
import async_timeout
@ -14,7 +15,6 @@ import voluptuous as vol
# Typing imports
from homeassistant.core import HomeAssistant
from typing import Dict, Any
from homeassistant.const import CONF_NAME
from homeassistant.helpers import config_validation as cv

View File

@ -1,11 +1,11 @@
"""Google Assistant OAuth View."""
import logging
from typing import Dict, Any
# Typing imports
# if False:
from aiohttp.web import Request, Response
from typing import Dict, Any
from homeassistant.core import HomeAssistant
from homeassistant.components.http import HomeAssistantView

View File

@ -324,3 +324,11 @@ async def handle_devices_execute(hass, config, payload):
})
return {'commands': final_results}
def turned_off_response(message):
"""Return a device turned off response."""
return {
'requestId': message.get('requestId'),
'payload': {'errorCode': 'deviceTurnedOff'}
}

View File

@ -1,4 +1,6 @@
"""Implement the Smart Home traits."""
import logging
from homeassistant.core import DOMAIN as HA_DOMAIN
from homeassistant.components import (
climate,
@ -25,6 +27,8 @@ from homeassistant.util import color as color_util, temperature as temp_util
from .const import ERR_VALUE_OUT_OF_RANGE
from .helpers import SmartHomeError
_LOGGER = logging.getLogger(__name__)
PREFIX_TRAITS = 'action.devices.traits.'
TRAIT_ONOFF = PREFIX_TRAITS + 'OnOff'
TRAIT_BRIGHTNESS = PREFIX_TRAITS + 'Brightness'
@ -317,7 +321,11 @@ class ColorTemperatureTrait(_Trait):
response = {}
temp = self.state.attributes.get(light.ATTR_COLOR_TEMP)
if temp is not None:
# Some faulty integrations might put 0 in here, raising exception.
if temp == 0:
_LOGGER.warning('Entity %s has incorrect color temperature %s',
self.state.entity_id, temp)
elif temp is not None:
response['color'] = {
'temperature':
color_util.color_temperature_mired_to_kelvin(temp)

View File

@ -0,0 +1,29 @@
{
"config": {
"abort": {
"already_configured": "Google Hangouts \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8",
"unknown": "\u05d0\u05d9\u05e8\u05e2\u05d4 \u05e9\u05d2\u05d9\u05d0\u05d4 \u05dc\u05d0 \u05d9\u05d3\u05d5\u05e2\u05d4."
},
"error": {
"invalid_2fa": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d3\u05d5 \u05e9\u05dc\u05d1\u05d9 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9, \u05d1\u05d1\u05e7\u05e9\u05d4 \u05e0\u05e1\u05d4 \u05e9\u05d5\u05d1.",
"invalid_2fa_method": "\u05d3\u05e8\u05da \u05dc\u05d0\u05d9\u05de\u05d5\u05ea \u05d3\u05d5 \u05e9\u05dc\u05d1\u05d9 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9\u05ea (\u05d0\u05de\u05ea \u05d1\u05d8\u05dc\u05e4\u05d5\u05df).",
"invalid_login": "\u05db\u05e0\u05d9\u05e1\u05d4 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9\u05ea, \u05e0\u05e1\u05d4 \u05e9\u05d5\u05d1."
},
"step": {
"2fa": {
"data": {
"2fa": "\u05e7\u05d5\u05d3 \u05d0\u05d9\u05de\u05d5\u05ea \u05d3\u05d5 \u05e9\u05dc\u05d1\u05d9"
},
"title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d3\u05d5 \u05e9\u05dc\u05d1\u05d9"
},
"user": {
"data": {
"email": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d3\u05d5\u05d0\"\u05dc",
"password": "\u05e1\u05d9\u05e1\u05de\u05d4"
},
"title": "\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05dc- Google Hangouts"
}
},
"title": "Google Hangouts"
}
}

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