mirror of
https://github.com/home-assistant/core.git
synced 2025-07-28 15:47:12 +00:00
commit
c93879074b
14
.coveragerc
14
.coveragerc
@ -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
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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/
|
||||
|
@ -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
|
||||
|
||||
if sys.platform == 'win32':
|
||||
asyncio.set_event_loop(asyncio.ProactorEventLoop())
|
||||
else:
|
||||
try:
|
||||
import uvloop
|
||||
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
|
||||
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,
|
||||
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__":
|
||||
|
@ -2,3 +2,4 @@
|
||||
from datetime import timedelta
|
||||
|
||||
ACCESS_TOKEN_EXPIRATION = timedelta(minutes=30)
|
||||
MFA_SESSION_EXPIRATION = timedelta(minutes=5)
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
325
homeassistant/auth/mfa_modules/notify.py
Normal file
325
homeassistant/auth/mfa_modules/notify.py
Normal 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,
|
||||
)
|
@ -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):
|
||||
|
@ -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',
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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,
|
||||
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)
|
||||
|
@ -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')
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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": {
|
||||
|
@ -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": {
|
||||
|
@ -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."
|
||||
|
@ -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."
|
||||
|
35
homeassistant/components/auth/.translations/he.json
Normal file
35
homeassistant/components/auth/.translations/he.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
16
homeassistant/components/auth/.translations/id.json
Normal file
16
homeassistant/components/auth/.translations/id.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
@ -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."
|
||||
|
@ -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."
|
||||
|
16
homeassistant/components/auth/.translations/nn.json
Normal file
16
homeassistant/components/auth/.translations/nn.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
@ -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."
|
||||
|
@ -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."
|
||||
|
15
homeassistant/components/auth/.translations/pt-BR.json
Normal file
15
homeassistant/components/auth/.translations/pt-BR.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
@ -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."
|
||||
|
@ -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."
|
||||
|
@ -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."
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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))
|
||||
|
||||
|
@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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),
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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,
|
||||
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)
|
||||
async_add_entities(SpcBinarySensor(zone)
|
||||
for zone in discovery_info[ATTR_DISCOVER_DEVICES]
|
||||
if _get_device_class(zone['type']))
|
||||
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)
|
||||
|
@ -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()
|
||||
|
@ -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()):
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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__)
|
||||
|
||||
|
@ -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)
|
||||
|
210
homeassistant/components/camera/logi_circle.py
Normal file
210
homeassistant/components/camera/logi_circle.py
Normal 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()
|
@ -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)
|
||||
|
@ -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."""
|
||||
|
@ -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."""
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -6,7 +6,7 @@
|
||||
},
|
||||
"step": {
|
||||
"confirm": {
|
||||
"description": "M\u00f6chten Sie Google Cast einrichten?",
|
||||
"description": "M\u00f6chtest du Google Cast einrichten?",
|
||||
"title": "Google Cast"
|
||||
}
|
||||
},
|
||||
|
15
homeassistant/components/cast/.translations/id.json
Normal file
15
homeassistant/components/cast/.translations/id.json
Normal 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"
|
||||
}
|
||||
}
|
15
homeassistant/components/cast/.translations/nn.json
Normal file
15
homeassistant/components/cast/.translations/nn.json
Normal 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"
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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__)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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."""
|
||||
|
@ -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')
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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',)
|
||||
|
||||
|
||||
|
@ -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()])
|
||||
|
||||
|
||||
|
47
homeassistant/components/config/device_registry.py
Normal file
47
homeassistant/components/config/device_registry.py
Normal 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())
|
@ -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.
|
||||
|
@ -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."""
|
||||
|
||||
|
146
homeassistant/components/cover/deconz.py
Normal file
146
homeassistant/components/cover/deconz.py
Normal 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),
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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):
|
||||
|
@ -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__)
|
||||
|
@ -28,6 +28,6 @@
|
||||
"title": "Opcions de configuraci\u00f3 addicionals per deCONZ"
|
||||
}
|
||||
},
|
||||
"title": "deCONZ"
|
||||
"title": "Passarel\u00b7la d'enlla\u00e7 deCONZ Zigbee"
|
||||
}
|
||||
}
|
@ -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": {
|
||||
|
@ -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"
|
||||
|
33
homeassistant/components/deconz/.translations/id.json
Normal file
33
homeassistant/components/deconz/.translations/id.json
Normal 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"
|
||||
}
|
||||
}
|
@ -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"
|
||||
|
33
homeassistant/components/deconz/.translations/nn.json
Normal file
33
homeassistant/components/deconz/.translations/nn.json
Normal 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"
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
||||
|
@ -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."""
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
65
homeassistant/components/device_tracker/huawei_lte.py
Normal file
65
homeassistant/components/device_tracker/huawei_lte.py
Normal 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 {}
|
@ -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'
|
||||
|
||||
|
@ -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),
|
||||
|
@ -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.
|
||||
|
135
homeassistant/components/edp_redy.py
Normal file
135
homeassistant/components/edp_redy.py
Normal 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
|
@ -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,8 +37,7 @@ 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,
|
||||
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)
|
||||
@ -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)
|
||||
|
||||
|
@ -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',
|
||||
|
@ -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__)
|
||||
|
||||
|
@ -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
|
||||
|
196
homeassistant/components/geo_location/geo_json_events.py
Normal file
196
homeassistant/components/geo_location/geo_json_events.py
Normal 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
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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'}
|
||||
}
|
||||
|
@ -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)
|
||||
|
29
homeassistant/components/hangouts/.translations/he.json
Normal file
29
homeassistant/components/hangouts/.translations/he.json
Normal 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
Loading…
x
Reference in New Issue
Block a user