mirror of
https://github.com/home-assistant/core.git
synced 2026-01-06 23:57:17 +01:00
Compare commits
280 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8413101148 | ||
|
|
8fb66c351e | ||
|
|
16ad9c2ae6 | ||
|
|
2ad938ed44 | ||
|
|
969b15a297 | ||
|
|
c8449d8f8a | ||
|
|
6992a6fe6d | ||
|
|
2ece671bfd | ||
|
|
c13e5fcb92 | ||
|
|
3783d1ce90 | ||
|
|
9ffcd2d86a | ||
|
|
66a8bede12 | ||
|
|
c2891b9905 | ||
|
|
b8c272258e | ||
|
|
4cb9ac72b4 | ||
|
|
cf8bd92d4d | ||
|
|
90b2257347 | ||
|
|
914d90a2bc | ||
|
|
e567b2281d | ||
|
|
bb6567f84c | ||
|
|
4f8fec6494 | ||
|
|
57979faa9c | ||
|
|
994b829cb4 | ||
|
|
37fd438717 | ||
|
|
5e301dd599 | ||
|
|
3d5b3fb6ff | ||
|
|
6c5f98668e | ||
|
|
ef0eab0f40 | ||
|
|
dd9d53c83e | ||
|
|
89d856d147 | ||
|
|
156c6e2025 | ||
|
|
d21d7cef4c | ||
|
|
249981de96 | ||
|
|
8e173f1658 | ||
|
|
ced5eeacc2 | ||
|
|
4155e8a31f | ||
|
|
4ad76d8916 | ||
|
|
478eb48e93 | ||
|
|
d8ae079757 | ||
|
|
b0c2d24997 | ||
|
|
2e6cb2235c | ||
|
|
0009be595c | ||
|
|
7e7f9bc6ac | ||
|
|
ae63980152 | ||
|
|
a31501d99e | ||
|
|
6864a44b5a | ||
|
|
523af4fbca | ||
|
|
b9733d0d99 | ||
|
|
7ed8ed83e3 | ||
|
|
0e1fb74e1b | ||
|
|
1ce51bfbd6 | ||
|
|
cdb8361050 | ||
|
|
00c6f56cc8 | ||
|
|
b26506ad4a | ||
|
|
7bb5344942 | ||
|
|
ae5c4c7e13 | ||
|
|
507e8f8f12 | ||
|
|
5a15b2c036 | ||
|
|
977d86e7ca | ||
|
|
bd776c84bc | ||
|
|
68cd65567d | ||
|
|
ef07460792 | ||
|
|
f84a31871e | ||
|
|
b1ba11510b | ||
|
|
439f7978c3 | ||
|
|
85a724e289 | ||
|
|
df6239e0fc | ||
|
|
1be61df9c0 | ||
|
|
d1e1b9b38a | ||
|
|
975befd136 | ||
|
|
a708a81fa8 | ||
|
|
18d19fde0b | ||
|
|
1f0d113688 | ||
|
|
121abb450a | ||
|
|
1be388c587 | ||
|
|
994e2b6624 | ||
|
|
dbd0763f83 | ||
|
|
e1b2e00cf6 | ||
|
|
3c5e62d47e | ||
|
|
3be301fac9 | ||
|
|
9c3251b5f0 | ||
|
|
cb44607e96 | ||
|
|
1c8ef4e196 | ||
|
|
9e1fa7ef42 | ||
|
|
7c95e96ce8 | ||
|
|
21b88f2fe8 | ||
|
|
81d3161a5e | ||
|
|
8beb349e88 | ||
|
|
c105045dab | ||
|
|
b901a26c47 | ||
|
|
8ec550d6e0 | ||
|
|
6827256586 | ||
|
|
ef193b0f64 | ||
|
|
e782e2c0f3 | ||
|
|
ec2e94425e | ||
|
|
9f0adc16ad | ||
|
|
fa88d918b1 | ||
|
|
3800f00564 | ||
|
|
2ad0bd4036 | ||
|
|
70412fc0ba | ||
|
|
e4425e6a37 | ||
|
|
fdbab3e20c | ||
|
|
f2e399ccf3 | ||
|
|
07840f5397 | ||
|
|
c09e7e620f | ||
|
|
92e26495da | ||
|
|
061859cc4d | ||
|
|
45452e510c | ||
|
|
279ead2085 | ||
|
|
649f17fe47 | ||
|
|
e9e5bce10c | ||
|
|
e41ce1d6ec | ||
|
|
bc21a1b944 | ||
|
|
834077190f | ||
|
|
2a210607d3 | ||
|
|
1ff1639cef | ||
|
|
5eccfc2604 | ||
|
|
2469bc7e2e | ||
|
|
d540a084dd | ||
|
|
11eb29f520 | ||
|
|
e4d41fe313 | ||
|
|
b5e7414be2 | ||
|
|
83b0ef4e26 | ||
|
|
b682e48e12 | ||
|
|
e52ba87af1 | ||
|
|
6da0ae4d23 | ||
|
|
f8051a5698 | ||
|
|
2306d14b5d | ||
|
|
4035880003 | ||
|
|
9cfbd067d3 | ||
|
|
39fd70231f | ||
|
|
dc460f4d6a | ||
|
|
c31035d348 | ||
|
|
555184a4b7 | ||
|
|
e64e84ad7a | ||
|
|
1777270aa2 | ||
|
|
f5df567d09 | ||
|
|
899c2057b7 | ||
|
|
1b384c322a | ||
|
|
f4e84fbf84 | ||
|
|
d393380122 | ||
|
|
d0e4c95bbc | ||
|
|
34e1f1b6da | ||
|
|
6d432d19fe | ||
|
|
486efa9aba | ||
|
|
0ad9fcd8a0 | ||
|
|
c7f7912bca | ||
|
|
e776f88eec | ||
|
|
ee5d49a033 | ||
|
|
051903d30c | ||
|
|
91e1ae035e | ||
|
|
10a2ecd1d6 | ||
|
|
800eb4d86a | ||
|
|
e3a2e58623 | ||
|
|
ea073b5e87 | ||
|
|
619d01150f | ||
|
|
6540d2e073 | ||
|
|
69b694ff26 | ||
|
|
9e21765173 | ||
|
|
c0830f1c20 | ||
|
|
985f96662e | ||
|
|
e0229b799d | ||
|
|
3fbb56d5fb | ||
|
|
3a60c8bbed | ||
|
|
f411fb89e6 | ||
|
|
1b5cfa7331 | ||
|
|
39647a15ae | ||
|
|
ba2e43600e | ||
|
|
c998a55fe7 | ||
|
|
da8f93dca2 | ||
|
|
50daef9a52 | ||
|
|
45f12dd3c7 | ||
|
|
6aee535d7c | ||
|
|
2342709803 | ||
|
|
272be7cdae | ||
|
|
31fbfed0a6 | ||
|
|
b7486e5605 | ||
|
|
e8218c4b29 | ||
|
|
d3fed52254 | ||
|
|
1205eaaa22 | ||
|
|
69934a9598 | ||
|
|
f24773933c | ||
|
|
e17e080639 | ||
|
|
055e35b297 | ||
|
|
81604a9326 | ||
|
|
a0e9f9f218 | ||
|
|
0ab3e7a92a | ||
|
|
9512bb9587 | ||
|
|
da916d7b27 | ||
|
|
b370b6a4e4 | ||
|
|
1911168855 | ||
|
|
f98629b895 | ||
|
|
dc01b17260 | ||
|
|
ef61c0c3a4 | ||
|
|
664eae72d1 | ||
|
|
86658f310d | ||
|
|
a29f867908 | ||
|
|
28de2d6f75 | ||
|
|
37d98474d5 | ||
|
|
5116f02290 | ||
|
|
2233d7ca98 | ||
|
|
f58425dd3c | ||
|
|
39d19f2183 | ||
|
|
99c4c65f69 | ||
|
|
61901496ec | ||
|
|
0ab65f1ac5 | ||
|
|
debdc707e9 | ||
|
|
fcc918a146 | ||
|
|
b6bc0097b8 | ||
|
|
d556edae31 | ||
|
|
1fb2ea70c2 | ||
|
|
4cbcb4c3a2 | ||
|
|
d071df0dec | ||
|
|
f09f153014 | ||
|
|
1d8678c431 | ||
|
|
51c30980df | ||
|
|
cb20c9b1ea | ||
|
|
a7db2ebbe1 | ||
|
|
61721478f3 | ||
|
|
47fa928425 | ||
|
|
10a7accd00 | ||
|
|
527585ff9c | ||
|
|
2f15a40e97 | ||
|
|
ccef9a3e43 | ||
|
|
479dfd1710 | ||
|
|
34ad4bd32d | ||
|
|
6031801206 | ||
|
|
9cfe0db3c8 | ||
|
|
8ef2cfa364 | ||
|
|
12e69202f8 | ||
|
|
e4b2ae29bd | ||
|
|
ac4674fdb0 | ||
|
|
f86702e8ab | ||
|
|
9a84f8b763 | ||
|
|
6a32b9bf87 | ||
|
|
b152becbe0 | ||
|
|
c41aa12d1d | ||
|
|
8a81ee3b4f | ||
|
|
5e1836f3a2 | ||
|
|
9ea3be4dc1 | ||
|
|
bce47eb9a4 | ||
|
|
018bd8544c | ||
|
|
bfb9f2a00b | ||
|
|
c7a8f1143c | ||
|
|
dbe44c076e | ||
|
|
3246b49a45 | ||
|
|
0c7d46927e | ||
|
|
f6935b5d27 | ||
|
|
91e8680fc5 | ||
|
|
6f2000f5e2 | ||
|
|
ee180c51cf | ||
|
|
b63312ff2e | ||
|
|
59f8a73676 | ||
|
|
affd4e7df3 | ||
|
|
38928c4c0e | ||
|
|
48af5116b3 | ||
|
|
eb5f6efb43 | ||
|
|
7972d6a0c6 | ||
|
|
bdea9e1333 | ||
|
|
2f8d66ef2b | ||
|
|
589b23b7e2 | ||
|
|
2e5131bb21 | ||
|
|
2ff5b4ce95 | ||
|
|
f8a478946e | ||
|
|
623f6c841b | ||
|
|
0b6f2f5b91 | ||
|
|
3445dc1f00 | ||
|
|
a11c2a0bd8 | ||
|
|
95da41aa15 | ||
|
|
27401f4975 | ||
|
|
d902a9f279 | ||
|
|
03847e6c41 | ||
|
|
a4f9602405 | ||
|
|
5f214ffa98 | ||
|
|
8ee3b535ef | ||
|
|
951372491c | ||
|
|
eeb79476de | ||
|
|
1b2d0e7a6f | ||
|
|
3208ad27ac | ||
|
|
cf87b76b0c |
27
.coveragerc
27
.coveragerc
@@ -104,6 +104,9 @@ omit =
|
||||
homeassistant/components/fritzbox.py
|
||||
homeassistant/components/switch/fritzbox.py
|
||||
|
||||
homeassistant/components/ecovacs.py
|
||||
homeassistant/components/*/ecovacs.py
|
||||
|
||||
homeassistant/components/eufy.py
|
||||
homeassistant/components/*/eufy.py
|
||||
|
||||
@@ -113,6 +116,12 @@ omit =
|
||||
homeassistant/components/google.py
|
||||
homeassistant/components/*/google.py
|
||||
|
||||
homeassistant/components/hangouts/__init__.py
|
||||
homeassistant/components/hangouts/const.py
|
||||
homeassistant/components/hangouts/hangouts_bot.py
|
||||
homeassistant/components/hangouts/hangups_utils.py
|
||||
homeassistant/components/*/hangouts.py
|
||||
|
||||
homeassistant/components/hdmi_cec.py
|
||||
homeassistant/components/*/hdmi_cec.py
|
||||
|
||||
@@ -133,12 +142,13 @@ omit =
|
||||
|
||||
homeassistant/components/ihc/*
|
||||
homeassistant/components/*/ihc.py
|
||||
|
||||
homeassistant/components/insteon/*
|
||||
homeassistant/components/*/insteon.py
|
||||
|
||||
homeassistant/components/insteon_local.py
|
||||
homeassistant/components/*/insteon_local.py
|
||||
|
||||
homeassistant/components/insteon_plm/*
|
||||
homeassistant/components/*/insteon_plm.py
|
||||
|
||||
homeassistant/components/insteon_plm.py
|
||||
|
||||
homeassistant/components/ios.py
|
||||
homeassistant/components/*/ios.py
|
||||
@@ -215,6 +225,9 @@ omit =
|
||||
homeassistant/components/opencv.py
|
||||
homeassistant/components/*/opencv.py
|
||||
|
||||
homeassistant/components/openuv.py
|
||||
homeassistant/components/*/openuv.py
|
||||
|
||||
homeassistant/components/pilight.py
|
||||
homeassistant/components/*/pilight.py
|
||||
|
||||
@@ -440,6 +453,7 @@ omit =
|
||||
homeassistant/components/device_tracker/netgear.py
|
||||
homeassistant/components/device_tracker/nmap_tracker.py
|
||||
homeassistant/components/device_tracker/ping.py
|
||||
homeassistant/components/device_tracker/ritassist.py
|
||||
homeassistant/components/device_tracker/sky_hub.py
|
||||
homeassistant/components/device_tracker/snmp.py
|
||||
homeassistant/components/device_tracker/swisscom.py
|
||||
@@ -509,6 +523,7 @@ omit =
|
||||
homeassistant/components/media_player/denon.py
|
||||
homeassistant/components/media_player/denonavr.py
|
||||
homeassistant/components/media_player/directv.py
|
||||
homeassistant/components/media_player/dlna_dmr.py
|
||||
homeassistant/components/media_player/dunehd.py
|
||||
homeassistant/components/media_player/emby.py
|
||||
homeassistant/components/media_player/epson.py
|
||||
@@ -532,6 +547,7 @@ omit =
|
||||
homeassistant/components/media_player/pandora.py
|
||||
homeassistant/components/media_player/philips_js.py
|
||||
homeassistant/components/media_player/pioneer.py
|
||||
homeassistant/components/media_player/pjlink.py
|
||||
homeassistant/components/media_player/plex.py
|
||||
homeassistant/components/media_player/roku.py
|
||||
homeassistant/components/media_player/russound_rio.py
|
||||
@@ -632,6 +648,7 @@ omit =
|
||||
homeassistant/components/sensor/eddystone_temperature.py
|
||||
homeassistant/components/sensor/eliqonline.py
|
||||
homeassistant/components/sensor/emoncms.py
|
||||
homeassistant/components/sensor/enphase_envoy.py
|
||||
homeassistant/components/sensor/envirophat.py
|
||||
homeassistant/components/sensor/etherscan.py
|
||||
homeassistant/components/sensor/fastdotcom.py
|
||||
@@ -676,7 +693,9 @@ omit =
|
||||
homeassistant/components/sensor/mvglive.py
|
||||
homeassistant/components/sensor/nederlandse_spoorwegen.py
|
||||
homeassistant/components/sensor/netdata.py
|
||||
homeassistant/components/sensor/netdata_public.py
|
||||
homeassistant/components/sensor/neurio_energy.py
|
||||
homeassistant/components/sensor/noaa_tides.py
|
||||
homeassistant/components/sensor/nsw_fuel_station.py
|
||||
homeassistant/components/sensor/nut.py
|
||||
homeassistant/components/sensor/nzbget.py
|
||||
|
||||
@@ -13,7 +13,8 @@ matrix:
|
||||
- python: "3.5.3"
|
||||
env: TOXENV=typing
|
||||
- python: "3.5.3"
|
||||
env: TOXENV=py35
|
||||
env: TOXENV=cov
|
||||
after_success: coveralls
|
||||
- python: "3.6"
|
||||
env: TOXENV=py36
|
||||
- python: "3.7"
|
||||
@@ -45,4 +46,3 @@ deploy:
|
||||
on:
|
||||
branch: dev
|
||||
condition: $TOXENV = lint
|
||||
after_success: coveralls
|
||||
|
||||
@@ -87,6 +87,8 @@ homeassistant/components/*/axis.py @kane610
|
||||
homeassistant/components/*/bmw_connected_drive.py @ChristianKuehnel
|
||||
homeassistant/components/*/broadlink.py @danielhiversen
|
||||
homeassistant/components/*/deconz.py @kane610
|
||||
homeassistant/components/ecovacs.py @OverloadUT
|
||||
homeassistant/components/*/ecovacs.py @OverloadUT
|
||||
homeassistant/components/eight_sleep.py @mezz64
|
||||
homeassistant/components/*/eight_sleep.py @mezz64
|
||||
homeassistant/components/hive.py @Rendili @KJonline
|
||||
@@ -98,6 +100,8 @@ homeassistant/components/konnected.py @heythisisnate
|
||||
homeassistant/components/*/konnected.py @heythisisnate
|
||||
homeassistant/components/matrix.py @tinloaf
|
||||
homeassistant/components/*/matrix.py @tinloaf
|
||||
homeassistant/components/openuv.py @bachya
|
||||
homeassistant/components/*/openuv.py @bachya
|
||||
homeassistant/components/qwikswitch.py @kellerza
|
||||
homeassistant/components/*/qwikswitch.py @kellerza
|
||||
homeassistant/components/rainmachine/* @bachya
|
||||
|
||||
@@ -10,7 +10,6 @@ LABEL maintainer="Paulus Schoutsen <Paulus@PaulusSchoutsen.nl>"
|
||||
#ENV INSTALL_OPENALPR no
|
||||
#ENV INSTALL_FFMPEG no
|
||||
#ENV INSTALL_LIBCEC no
|
||||
#ENV INSTALL_PHANTOMJS no
|
||||
#ENV INSTALL_SSOCR no
|
||||
#ENV INSTALL_IPERF3 no
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Home Assistant |Build Status| |Coverage Status| |Chat Status|
|
||||
=============================================================
|
||||
Home Assistant |Build Status| |Coverage Status| |Chat Status| |Reviewed by Hound|
|
||||
=================================================================================
|
||||
|
||||
Home Assistant is a home automation platform running on Python 3. It is able to track and control all devices at home and offer a platform for automating control.
|
||||
|
||||
@@ -33,6 +33,8 @@ of a component, check the `Home Assistant help section <https://home-assistant.i
|
||||
:target: https://coveralls.io/r/home-assistant/home-assistant?branch=master
|
||||
.. |Chat Status| image:: https://img.shields.io/discord/330944238910963714.svg
|
||||
:target: https://discord.gg/c5DvZ4e
|
||||
.. |Reviewed by Hound| image:: https://img.shields.io/badge/Reviewed_by-Hound-8E64B0.svg
|
||||
:target: https://houndci.com
|
||||
.. |screenshot-states| image:: https://raw.github.com/home-assistant/home-assistant/master/docs/screenshots.png
|
||||
:target: https://home-assistant.io/demo/
|
||||
.. |screenshot-components| image:: https://raw.github.com/home-assistant/home-assistant/dev/docs/screenshot-components.png
|
||||
|
||||
@@ -60,14 +60,6 @@ loader module
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
remote module
|
||||
---------------------------
|
||||
|
||||
.. automodule:: homeassistant.remote
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
Module contents
|
||||
---------------
|
||||
|
||||
@@ -2,18 +2,28 @@
|
||||
import asyncio
|
||||
import logging
|
||||
from collections import OrderedDict
|
||||
from typing import Any, Dict, List, Optional, Tuple, cast
|
||||
|
||||
import jwt
|
||||
|
||||
from homeassistant import data_entry_flow
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.core import callback, HomeAssistant
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import models
|
||||
from . import auth_store
|
||||
from .providers import auth_provider_from_config
|
||||
from . import auth_store, models
|
||||
from .mfa_modules import auth_mfa_module_from_config, MultiFactorAuthModule
|
||||
from .providers import auth_provider_from_config, AuthProvider, LoginFlow
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_MfaModuleDict = Dict[str, MultiFactorAuthModule]
|
||||
_ProviderKey = Tuple[str, Optional[str]]
|
||||
_ProviderDict = Dict[_ProviderKey, AuthProvider]
|
||||
|
||||
|
||||
async def auth_manager_from_config(hass, provider_configs):
|
||||
async def auth_manager_from_config(
|
||||
hass: HomeAssistant,
|
||||
provider_configs: List[Dict[str, Any]],
|
||||
module_configs: List[Dict[str, Any]]) -> 'AuthManager':
|
||||
"""Initialize an auth manager from config."""
|
||||
store = auth_store.AuthStore(hass)
|
||||
if provider_configs:
|
||||
@@ -21,9 +31,9 @@ async def auth_manager_from_config(hass, provider_configs):
|
||||
*[auth_provider_from_config(hass, store, config)
|
||||
for config in provider_configs])
|
||||
else:
|
||||
providers = []
|
||||
providers = ()
|
||||
# So returned auth providers are in same order as config
|
||||
provider_hash = OrderedDict()
|
||||
provider_hash = OrderedDict() # type: _ProviderDict
|
||||
for provider in providers:
|
||||
if provider is None:
|
||||
continue
|
||||
@@ -37,29 +47,53 @@ async def auth_manager_from_config(hass, provider_configs):
|
||||
continue
|
||||
|
||||
provider_hash[key] = provider
|
||||
manager = AuthManager(hass, store, provider_hash)
|
||||
|
||||
if module_configs:
|
||||
modules = await asyncio.gather(
|
||||
*[auth_mfa_module_from_config(hass, config)
|
||||
for config in module_configs])
|
||||
else:
|
||||
modules = ()
|
||||
# So returned auth modules are in same order as config
|
||||
module_hash = OrderedDict() # type: _MfaModuleDict
|
||||
for module in modules:
|
||||
if module is None:
|
||||
continue
|
||||
|
||||
if module.id in module_hash:
|
||||
_LOGGER.error(
|
||||
'Found duplicate multi-factor module: %s. Please add unique '
|
||||
'IDs if you want to have the same module twice.', module.id)
|
||||
continue
|
||||
|
||||
module_hash[module.id] = module
|
||||
|
||||
manager = AuthManager(hass, store, provider_hash, module_hash)
|
||||
return manager
|
||||
|
||||
|
||||
class AuthManager:
|
||||
"""Manage the authentication for Home Assistant."""
|
||||
|
||||
def __init__(self, hass, store, providers):
|
||||
def __init__(self, hass: HomeAssistant, store: auth_store.AuthStore,
|
||||
providers: _ProviderDict, mfa_modules: _MfaModuleDict) \
|
||||
-> None:
|
||||
"""Initialize the auth manager."""
|
||||
self.hass = hass
|
||||
self._store = store
|
||||
self._providers = providers
|
||||
self._mfa_modules = mfa_modules
|
||||
self.login_flow = data_entry_flow.FlowManager(
|
||||
hass, self._async_create_login_flow,
|
||||
self._async_finish_login_flow)
|
||||
self._access_tokens = OrderedDict()
|
||||
|
||||
@property
|
||||
def active(self):
|
||||
def active(self) -> bool:
|
||||
"""Return if any auth providers are registered."""
|
||||
return bool(self._providers)
|
||||
|
||||
@property
|
||||
def support_legacy(self):
|
||||
def support_legacy(self) -> bool:
|
||||
"""
|
||||
Return if legacy_api_password auth providers are registered.
|
||||
|
||||
@@ -71,19 +105,39 @@ class AuthManager:
|
||||
return False
|
||||
|
||||
@property
|
||||
def auth_providers(self):
|
||||
def auth_providers(self) -> List[AuthProvider]:
|
||||
"""Return a list of available auth providers."""
|
||||
return list(self._providers.values())
|
||||
|
||||
async def async_get_users(self):
|
||||
@property
|
||||
def auth_mfa_modules(self) -> List[MultiFactorAuthModule]:
|
||||
"""Return a list of available auth modules."""
|
||||
return list(self._mfa_modules.values())
|
||||
|
||||
def get_auth_mfa_module(self, module_id: str) \
|
||||
-> Optional[MultiFactorAuthModule]:
|
||||
"""Return an multi-factor auth module, None if not found."""
|
||||
return self._mfa_modules.get(module_id)
|
||||
|
||||
async def async_get_users(self) -> List[models.User]:
|
||||
"""Retrieve all users."""
|
||||
return await self._store.async_get_users()
|
||||
|
||||
async def async_get_user(self, user_id):
|
||||
async def async_get_user(self, user_id: str) -> Optional[models.User]:
|
||||
"""Retrieve a user."""
|
||||
return await self._store.async_get_user(user_id)
|
||||
|
||||
async def async_create_system_user(self, name):
|
||||
async def async_get_user_by_credentials(
|
||||
self, credentials: models.Credentials) -> Optional[models.User]:
|
||||
"""Get a user by credential, return None if not found."""
|
||||
for user in await self.async_get_users():
|
||||
for creds in user.credentials:
|
||||
if creds.id == credentials.id:
|
||||
return user
|
||||
|
||||
return None
|
||||
|
||||
async def async_create_system_user(self, name: str) -> models.User:
|
||||
"""Create a system user."""
|
||||
return await self._store.async_create_user(
|
||||
name=name,
|
||||
@@ -91,27 +145,27 @@ class AuthManager:
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
async def async_create_user(self, name):
|
||||
async def async_create_user(self, name: str) -> models.User:
|
||||
"""Create a user."""
|
||||
kwargs = {
|
||||
'name': name,
|
||||
'is_active': True,
|
||||
}
|
||||
} # type: Dict[str, Any]
|
||||
|
||||
if await self._user_should_be_owner():
|
||||
kwargs['is_owner'] = True
|
||||
|
||||
return await self._store.async_create_user(**kwargs)
|
||||
|
||||
async def async_get_or_create_user(self, credentials):
|
||||
async def async_get_or_create_user(self, credentials: models.Credentials) \
|
||||
-> models.User:
|
||||
"""Get or create a user."""
|
||||
if not credentials.is_new:
|
||||
for user in await self._store.async_get_users():
|
||||
for creds in user.credentials:
|
||||
if creds.id == credentials.id:
|
||||
return user
|
||||
|
||||
raise ValueError('Unable to find the user.')
|
||||
user = await self.async_get_user_by_credentials(credentials)
|
||||
if user is None:
|
||||
raise ValueError('Unable to find the user.')
|
||||
else:
|
||||
return user
|
||||
|
||||
auth_provider = self._async_get_auth_provider(credentials)
|
||||
|
||||
@@ -123,15 +177,16 @@ class AuthManager:
|
||||
|
||||
return await self._store.async_create_user(
|
||||
credentials=credentials,
|
||||
name=info.get('name'),
|
||||
is_active=info.get('is_active', False)
|
||||
name=info.name,
|
||||
is_active=info.is_active,
|
||||
)
|
||||
|
||||
async def async_link_user(self, user, credentials):
|
||||
async def async_link_user(self, user: models.User,
|
||||
credentials: models.Credentials) -> None:
|
||||
"""Link credentials to an existing user."""
|
||||
await self._store.async_link_user(user, credentials)
|
||||
|
||||
async def async_remove_user(self, user):
|
||||
async def async_remove_user(self, user: models.User) -> None:
|
||||
"""Remove a user."""
|
||||
tasks = [
|
||||
self.async_remove_credentials(credentials)
|
||||
@@ -143,27 +198,68 @@ class AuthManager:
|
||||
|
||||
await self._store.async_remove_user(user)
|
||||
|
||||
async def async_activate_user(self, user):
|
||||
async def async_activate_user(self, user: models.User) -> None:
|
||||
"""Activate a user."""
|
||||
await self._store.async_activate_user(user)
|
||||
|
||||
async def async_deactivate_user(self, user):
|
||||
async def async_deactivate_user(self, user: models.User) -> None:
|
||||
"""Deactivate a user."""
|
||||
if user.is_owner:
|
||||
raise ValueError('Unable to deactive the owner')
|
||||
await self._store.async_deactivate_user(user)
|
||||
|
||||
async def async_remove_credentials(self, credentials):
|
||||
async def async_remove_credentials(
|
||||
self, credentials: models.Credentials) -> None:
|
||||
"""Remove credentials."""
|
||||
provider = self._async_get_auth_provider(credentials)
|
||||
|
||||
if (provider is not None and
|
||||
hasattr(provider, 'async_will_remove_credentials')):
|
||||
await provider.async_will_remove_credentials(credentials)
|
||||
# https://github.com/python/mypy/issues/1424
|
||||
await provider.async_will_remove_credentials( # type: ignore
|
||||
credentials)
|
||||
|
||||
await self._store.async_remove_credentials(credentials)
|
||||
|
||||
async def async_create_refresh_token(self, user, client_id=None):
|
||||
async def async_enable_user_mfa(self, user: models.User,
|
||||
mfa_module_id: str, data: Any) -> None:
|
||||
"""Enable a multi-factor auth module for user."""
|
||||
if user.system_generated:
|
||||
raise ValueError('System generated users cannot enable '
|
||||
'multi-factor auth module.')
|
||||
|
||||
module = self.get_auth_mfa_module(mfa_module_id)
|
||||
if module is None:
|
||||
raise ValueError('Unable find multi-factor auth module: {}'
|
||||
.format(mfa_module_id))
|
||||
|
||||
await module.async_setup_user(user.id, data)
|
||||
|
||||
async def async_disable_user_mfa(self, user: models.User,
|
||||
mfa_module_id: str) -> None:
|
||||
"""Disable a multi-factor auth module for user."""
|
||||
if user.system_generated:
|
||||
raise ValueError('System generated users cannot disable '
|
||||
'multi-factor auth module.')
|
||||
|
||||
module = self.get_auth_mfa_module(mfa_module_id)
|
||||
if module is None:
|
||||
raise ValueError('Unable find multi-factor auth module: {}'
|
||||
.format(mfa_module_id))
|
||||
|
||||
await module.async_depose_user(user.id)
|
||||
|
||||
async def async_get_enabled_mfa(self, user: models.User) -> Dict[str, str]:
|
||||
"""List enabled mfa modules for user."""
|
||||
modules = OrderedDict() # type: Dict[str, str]
|
||||
for module_id, module in self._mfa_modules.items():
|
||||
if await module.async_is_user_setup(user.id):
|
||||
modules[module_id] = module.name
|
||||
return modules
|
||||
|
||||
async def async_create_refresh_token(self, user: models.User,
|
||||
client_id: Optional[str] = None) \
|
||||
-> models.RefreshToken:
|
||||
"""Create a new refresh token for a user."""
|
||||
if not user.is_active:
|
||||
raise ValueError('User is not active')
|
||||
@@ -178,59 +274,119 @@ class AuthManager:
|
||||
|
||||
return await self._store.async_create_refresh_token(user, client_id)
|
||||
|
||||
async def async_get_refresh_token(self, token):
|
||||
async def async_get_refresh_token(
|
||||
self, token_id: str) -> Optional[models.RefreshToken]:
|
||||
"""Get refresh token by id."""
|
||||
return await self._store.async_get_refresh_token(token_id)
|
||||
|
||||
async def async_get_refresh_token_by_token(
|
||||
self, token: str) -> Optional[models.RefreshToken]:
|
||||
"""Get refresh token by token."""
|
||||
return await self._store.async_get_refresh_token(token)
|
||||
return await self._store.async_get_refresh_token_by_token(token)
|
||||
|
||||
async def async_remove_refresh_token(self,
|
||||
refresh_token: models.RefreshToken) \
|
||||
-> None:
|
||||
"""Delete a refresh token."""
|
||||
await self._store.async_remove_refresh_token(refresh_token)
|
||||
|
||||
@callback
|
||||
def async_create_access_token(self, refresh_token):
|
||||
def async_create_access_token(self,
|
||||
refresh_token: models.RefreshToken) -> str:
|
||||
"""Create a new access token."""
|
||||
access_token = models.AccessToken(refresh_token=refresh_token)
|
||||
self._access_tokens[access_token.token] = access_token
|
||||
return access_token
|
||||
# pylint: disable=no-self-use
|
||||
return jwt.encode({
|
||||
'iss': refresh_token.id,
|
||||
'iat': dt_util.utcnow(),
|
||||
'exp': dt_util.utcnow() + refresh_token.access_token_expiration,
|
||||
}, refresh_token.jwt_key, algorithm='HS256').decode()
|
||||
|
||||
@callback
|
||||
def async_get_access_token(self, token):
|
||||
"""Get an access token."""
|
||||
tkn = self._access_tokens.get(token)
|
||||
|
||||
if tkn is None:
|
||||
_LOGGER.debug('Attempt to get non-existing access token')
|
||||
async def async_validate_access_token(
|
||||
self, token: str) -> Optional[models.RefreshToken]:
|
||||
"""Return refresh token if an access token is valid."""
|
||||
try:
|
||||
unverif_claims = jwt.decode(token, verify=False)
|
||||
except jwt.InvalidTokenError:
|
||||
return None
|
||||
|
||||
if tkn.expired or not tkn.refresh_token.user.is_active:
|
||||
if tkn.expired:
|
||||
_LOGGER.debug('Attempt to get expired access token')
|
||||
else:
|
||||
_LOGGER.debug('Attempt to get access token for inactive user')
|
||||
self._access_tokens.pop(token)
|
||||
refresh_token = await self.async_get_refresh_token(
|
||||
cast(str, unverif_claims.get('iss')))
|
||||
|
||||
if refresh_token is None:
|
||||
jwt_key = ''
|
||||
issuer = ''
|
||||
else:
|
||||
jwt_key = refresh_token.jwt_key
|
||||
issuer = refresh_token.id
|
||||
|
||||
try:
|
||||
jwt.decode(
|
||||
token,
|
||||
jwt_key,
|
||||
leeway=10,
|
||||
issuer=issuer,
|
||||
algorithms=['HS256']
|
||||
)
|
||||
except jwt.InvalidTokenError:
|
||||
return None
|
||||
|
||||
return tkn
|
||||
if refresh_token is None or not refresh_token.user.is_active:
|
||||
return None
|
||||
|
||||
async def _async_create_login_flow(self, handler, *, source, data):
|
||||
return refresh_token
|
||||
|
||||
async def _async_create_login_flow(
|
||||
self, handler: _ProviderKey, *, context: Optional[Dict],
|
||||
data: Optional[Any]) -> data_entry_flow.FlowHandler:
|
||||
"""Create a login flow."""
|
||||
auth_provider = self._providers[handler]
|
||||
|
||||
return await auth_provider.async_credential_flow()
|
||||
return await auth_provider.async_login_flow(context)
|
||||
|
||||
async def _async_finish_login_flow(self, result):
|
||||
"""Result of a credential login flow."""
|
||||
async def _async_finish_login_flow(
|
||||
self, flow: LoginFlow, result: Dict[str, Any]) \
|
||||
-> Dict[str, Any]:
|
||||
"""Return a user as result of login flow."""
|
||||
if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
|
||||
return None
|
||||
return result
|
||||
|
||||
# we got final result
|
||||
if isinstance(result['data'], models.User):
|
||||
result['result'] = result['data']
|
||||
return result
|
||||
|
||||
auth_provider = self._providers[result['handler']]
|
||||
return await auth_provider.async_get_or_create_credentials(
|
||||
credentials = await auth_provider.async_get_or_create_credentials(
|
||||
result['data'])
|
||||
|
||||
if flow.context is not None and flow.context.get('credential_only'):
|
||||
result['result'] = credentials
|
||||
return result
|
||||
|
||||
# multi-factor module cannot enabled for new credential
|
||||
# which has not linked to a user yet
|
||||
if auth_provider.support_mfa and not credentials.is_new:
|
||||
user = await self.async_get_user_by_credentials(credentials)
|
||||
if user is not None:
|
||||
modules = await self.async_get_enabled_mfa(user)
|
||||
|
||||
if modules:
|
||||
flow.user = user
|
||||
flow.available_mfa_modules = modules
|
||||
return await flow.async_step_select_mfa_module()
|
||||
|
||||
result['result'] = await self.async_get_or_create_user(credentials)
|
||||
return result
|
||||
|
||||
@callback
|
||||
def _async_get_auth_provider(self, credentials):
|
||||
"""Helper to get auth provider from a set of credentials."""
|
||||
def _async_get_auth_provider(
|
||||
self, credentials: models.Credentials) -> Optional[AuthProvider]:
|
||||
"""Get auth provider from a set of credentials."""
|
||||
auth_provider_key = (credentials.auth_provider_type,
|
||||
credentials.auth_provider_id)
|
||||
return self._providers.get(auth_provider_key)
|
||||
|
||||
async def _user_should_be_owner(self):
|
||||
async def _user_should_be_owner(self) -> bool:
|
||||
"""Determine if user should be owner.
|
||||
|
||||
A user should be an owner if it is the first non-system user that is
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
"""Storage for auth models."""
|
||||
from collections import OrderedDict
|
||||
from datetime import timedelta
|
||||
from logging import getLogger
|
||||
from typing import Any, Dict, List, Optional # noqa: F401
|
||||
import hmac
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import models
|
||||
@@ -19,35 +23,41 @@ class AuthStore:
|
||||
called that needs it.
|
||||
"""
|
||||
|
||||
def __init__(self, hass):
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
"""Initialize the auth store."""
|
||||
self.hass = hass
|
||||
self._users = None
|
||||
self._users = None # type: Optional[Dict[str, models.User]]
|
||||
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
|
||||
|
||||
async def async_get_users(self):
|
||||
async def async_get_users(self) -> List[models.User]:
|
||||
"""Retrieve all users."""
|
||||
if self._users is None:
|
||||
await self.async_load()
|
||||
await self._async_load()
|
||||
assert self._users is not None
|
||||
|
||||
return list(self._users.values())
|
||||
|
||||
async def async_get_user(self, user_id):
|
||||
async def async_get_user(self, user_id: str) -> Optional[models.User]:
|
||||
"""Retrieve a user by id."""
|
||||
if self._users is None:
|
||||
await self.async_load()
|
||||
await self._async_load()
|
||||
assert self._users is not None
|
||||
|
||||
return self._users.get(user_id)
|
||||
|
||||
async def async_create_user(self, name, is_owner=None, is_active=None,
|
||||
system_generated=None, credentials=None):
|
||||
async def async_create_user(
|
||||
self, name: Optional[str], is_owner: Optional[bool] = None,
|
||||
is_active: Optional[bool] = None,
|
||||
system_generated: Optional[bool] = None,
|
||||
credentials: Optional[models.Credentials] = None) -> models.User:
|
||||
"""Create a new user."""
|
||||
if self._users is None:
|
||||
await self.async_load()
|
||||
await self._async_load()
|
||||
assert self._users is not None
|
||||
|
||||
kwargs = {
|
||||
'name': name
|
||||
}
|
||||
} # type: Dict[str, Any]
|
||||
|
||||
if is_owner is not None:
|
||||
kwargs['is_owner'] = is_owner
|
||||
@@ -63,36 +73,46 @@ class AuthStore:
|
||||
self._users[new_user.id] = new_user
|
||||
|
||||
if credentials is None:
|
||||
await self.async_save()
|
||||
self._async_schedule_save()
|
||||
return new_user
|
||||
|
||||
# Saving is done inside the link.
|
||||
await self.async_link_user(new_user, credentials)
|
||||
return new_user
|
||||
|
||||
async def async_link_user(self, user, credentials):
|
||||
async def async_link_user(self, user: models.User,
|
||||
credentials: models.Credentials) -> None:
|
||||
"""Add credentials to an existing user."""
|
||||
user.credentials.append(credentials)
|
||||
await self.async_save()
|
||||
self._async_schedule_save()
|
||||
credentials.is_new = False
|
||||
|
||||
async def async_remove_user(self, user):
|
||||
async def async_remove_user(self, user: models.User) -> None:
|
||||
"""Remove a user."""
|
||||
self._users.pop(user.id)
|
||||
await self.async_save()
|
||||
if self._users is None:
|
||||
await self._async_load()
|
||||
assert self._users is not None
|
||||
|
||||
async def async_activate_user(self, user):
|
||||
self._users.pop(user.id)
|
||||
self._async_schedule_save()
|
||||
|
||||
async def async_activate_user(self, user: models.User) -> None:
|
||||
"""Activate a user."""
|
||||
user.is_active = True
|
||||
await self.async_save()
|
||||
self._async_schedule_save()
|
||||
|
||||
async def async_deactivate_user(self, user):
|
||||
async def async_deactivate_user(self, user: models.User) -> None:
|
||||
"""Activate a user."""
|
||||
user.is_active = False
|
||||
await self.async_save()
|
||||
self._async_schedule_save()
|
||||
|
||||
async def async_remove_credentials(self, credentials):
|
||||
async def async_remove_credentials(
|
||||
self, credentials: models.Credentials) -> None:
|
||||
"""Remove credentials."""
|
||||
if self._users is None:
|
||||
await self._async_load()
|
||||
assert self._users is not None
|
||||
|
||||
for user in self._users.values():
|
||||
found = None
|
||||
|
||||
@@ -105,28 +125,60 @@ class AuthStore:
|
||||
user.credentials.pop(found)
|
||||
break
|
||||
|
||||
await self.async_save()
|
||||
self._async_schedule_save()
|
||||
|
||||
async def async_create_refresh_token(self, user, client_id=None):
|
||||
async def async_create_refresh_token(
|
||||
self, user: models.User, client_id: Optional[str] = None) \
|
||||
-> models.RefreshToken:
|
||||
"""Create a new token for a user."""
|
||||
refresh_token = models.RefreshToken(user=user, client_id=client_id)
|
||||
user.refresh_tokens[refresh_token.token] = refresh_token
|
||||
await self.async_save()
|
||||
user.refresh_tokens[refresh_token.id] = refresh_token
|
||||
self._async_schedule_save()
|
||||
return refresh_token
|
||||
|
||||
async def async_get_refresh_token(self, token):
|
||||
"""Get refresh token by token."""
|
||||
async def async_remove_refresh_token(
|
||||
self, refresh_token: models.RefreshToken) -> None:
|
||||
"""Remove a refresh token."""
|
||||
if self._users is None:
|
||||
await self.async_load()
|
||||
await self._async_load()
|
||||
assert self._users is not None
|
||||
|
||||
for user in self._users.values():
|
||||
refresh_token = user.refresh_tokens.get(token)
|
||||
if user.refresh_tokens.pop(refresh_token.id, None):
|
||||
self._async_schedule_save()
|
||||
break
|
||||
|
||||
async def async_get_refresh_token(
|
||||
self, token_id: str) -> Optional[models.RefreshToken]:
|
||||
"""Get refresh token by id."""
|
||||
if self._users is None:
|
||||
await self._async_load()
|
||||
assert self._users is not None
|
||||
|
||||
for user in self._users.values():
|
||||
refresh_token = user.refresh_tokens.get(token_id)
|
||||
if refresh_token is not None:
|
||||
return refresh_token
|
||||
|
||||
return None
|
||||
|
||||
async def async_load(self):
|
||||
async def async_get_refresh_token_by_token(
|
||||
self, token: str) -> Optional[models.RefreshToken]:
|
||||
"""Get refresh token by token."""
|
||||
if self._users is None:
|
||||
await self._async_load()
|
||||
assert self._users is not None
|
||||
|
||||
found = None
|
||||
|
||||
for user in self._users.values():
|
||||
for refresh_token in user.refresh_tokens.values():
|
||||
if hmac.compare_digest(refresh_token.token, token):
|
||||
found = refresh_token
|
||||
|
||||
return found
|
||||
|
||||
async def _async_load(self) -> None:
|
||||
"""Load the users."""
|
||||
data = await self._store.async_load()
|
||||
|
||||
@@ -135,7 +187,7 @@ class AuthStore:
|
||||
if self._users is not None:
|
||||
return
|
||||
|
||||
users = OrderedDict()
|
||||
users = OrderedDict() # type: Dict[str, models.User]
|
||||
|
||||
if data is None:
|
||||
self._users = users
|
||||
@@ -153,34 +205,44 @@ class AuthStore:
|
||||
data=cred_dict['data'],
|
||||
))
|
||||
|
||||
refresh_tokens = OrderedDict()
|
||||
|
||||
for rt_dict in data['refresh_tokens']:
|
||||
# Filter out the old keys that don't have jwt_key (pre-0.76)
|
||||
if 'jwt_key' not in rt_dict:
|
||||
continue
|
||||
|
||||
created_at = dt_util.parse_datetime(rt_dict['created_at'])
|
||||
if created_at is None:
|
||||
getLogger(__name__).error(
|
||||
'Ignoring refresh token %(id)s with invalid created_at '
|
||||
'%(created_at)s for user_id %(user_id)s', rt_dict)
|
||||
continue
|
||||
token = models.RefreshToken(
|
||||
id=rt_dict['id'],
|
||||
user=users[rt_dict['user_id']],
|
||||
client_id=rt_dict['client_id'],
|
||||
created_at=dt_util.parse_datetime(rt_dict['created_at']),
|
||||
created_at=created_at,
|
||||
access_token_expiration=timedelta(
|
||||
seconds=rt_dict['access_token_expiration']),
|
||||
token=rt_dict['token'],
|
||||
jwt_key=rt_dict['jwt_key']
|
||||
)
|
||||
refresh_tokens[token.id] = token
|
||||
users[rt_dict['user_id']].refresh_tokens[token.token] = token
|
||||
|
||||
for ac_dict in data['access_tokens']:
|
||||
refresh_token = refresh_tokens[ac_dict['refresh_token_id']]
|
||||
token = models.AccessToken(
|
||||
refresh_token=refresh_token,
|
||||
created_at=dt_util.parse_datetime(ac_dict['created_at']),
|
||||
token=ac_dict['token'],
|
||||
)
|
||||
refresh_token.access_tokens.append(token)
|
||||
users[rt_dict['user_id']].refresh_tokens[token.id] = token
|
||||
|
||||
self._users = users
|
||||
|
||||
async def async_save(self):
|
||||
@callback
|
||||
def _async_schedule_save(self) -> None:
|
||||
"""Save users."""
|
||||
if self._users is None:
|
||||
return
|
||||
|
||||
self._store.async_delay_save(self._data_to_save, 1)
|
||||
|
||||
@callback
|
||||
def _data_to_save(self) -> Dict:
|
||||
"""Return the data to store."""
|
||||
assert self._users is not None
|
||||
|
||||
users = [
|
||||
{
|
||||
'id': user.id,
|
||||
@@ -213,28 +275,14 @@ class AuthStore:
|
||||
'access_token_expiration':
|
||||
refresh_token.access_token_expiration.total_seconds(),
|
||||
'token': refresh_token.token,
|
||||
'jwt_key': refresh_token.jwt_key,
|
||||
}
|
||||
for user in self._users.values()
|
||||
for refresh_token in user.refresh_tokens.values()
|
||||
]
|
||||
|
||||
access_tokens = [
|
||||
{
|
||||
'id': user.id,
|
||||
'refresh_token_id': refresh_token.id,
|
||||
'created_at': access_token.created_at.isoformat(),
|
||||
'token': access_token.token,
|
||||
}
|
||||
for user in self._users.values()
|
||||
for refresh_token in user.refresh_tokens.values()
|
||||
for access_token in refresh_token.access_tokens
|
||||
]
|
||||
|
||||
data = {
|
||||
return {
|
||||
'users': users,
|
||||
'credentials': credentials,
|
||||
'access_tokens': access_tokens,
|
||||
'refresh_tokens': refresh_tokens,
|
||||
}
|
||||
|
||||
await self._store.async_save(data, delay=1)
|
||||
|
||||
176
homeassistant/auth/mfa_modules/__init__.py
Normal file
176
homeassistant/auth/mfa_modules/__init__.py
Normal file
@@ -0,0 +1,176 @@
|
||||
"""Plugable auth modules for Home Assistant."""
|
||||
from datetime import timedelta
|
||||
import importlib
|
||||
import logging
|
||||
import types
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
import voluptuous as vol
|
||||
from voluptuous.humanize import humanize_error
|
||||
|
||||
from homeassistant import requirements, data_entry_flow
|
||||
from homeassistant.const import CONF_ID, CONF_NAME, CONF_TYPE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.util.decorator import Registry
|
||||
|
||||
MULTI_FACTOR_AUTH_MODULES = Registry()
|
||||
|
||||
MULTI_FACTOR_AUTH_MODULE_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_TYPE): str,
|
||||
vol.Optional(CONF_NAME): str,
|
||||
# Specify ID if you have two mfa auth module for same type.
|
||||
vol.Optional(CONF_ID): str,
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
SESSION_EXPIRATION = timedelta(minutes=5)
|
||||
|
||||
DATA_REQS = 'mfa_auth_module_reqs_processed'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MultiFactorAuthModule:
|
||||
"""Multi-factor Auth Module of validation function."""
|
||||
|
||||
DEFAULT_TITLE = 'Unnamed auth module'
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: Dict[str, Any]) -> None:
|
||||
"""Initialize an auth module."""
|
||||
self.hass = hass
|
||||
self.config = config
|
||||
|
||||
@property
|
||||
def id(self) -> str: # pylint: disable=invalid-name
|
||||
"""Return id of the auth module.
|
||||
|
||||
Default is same as type
|
||||
"""
|
||||
return self.config.get(CONF_ID, self.type)
|
||||
|
||||
@property
|
||||
def type(self) -> str:
|
||||
"""Return type of the module."""
|
||||
return self.config[CONF_TYPE] # type: ignore
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the name of the auth module."""
|
||||
return self.config.get(CONF_NAME, self.DEFAULT_TITLE)
|
||||
|
||||
# Implement by extending class
|
||||
|
||||
@property
|
||||
def input_schema(self) -> vol.Schema:
|
||||
"""Return a voluptuous schema to define mfa auth module's input."""
|
||||
raise NotImplementedError
|
||||
|
||||
async def async_setup_flow(self, user_id: str) -> 'SetupFlow':
|
||||
"""Return a data entry flow handler for setup module.
|
||||
|
||||
Mfa module should extend SetupFlow
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
async def async_setup_user(self, user_id: str, setup_data: Any) -> Any:
|
||||
"""Set up user for mfa auth module."""
|
||||
raise NotImplementedError
|
||||
|
||||
async def async_depose_user(self, user_id: str) -> None:
|
||||
"""Remove user from mfa module."""
|
||||
raise NotImplementedError
|
||||
|
||||
async def async_is_user_setup(self, user_id: str) -> bool:
|
||||
"""Return whether user is setup."""
|
||||
raise NotImplementedError
|
||||
|
||||
async def async_validation(
|
||||
self, user_id: str, user_input: Dict[str, Any]) -> bool:
|
||||
"""Return True if validation passed."""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class SetupFlow(data_entry_flow.FlowHandler):
|
||||
"""Handler for the setup flow."""
|
||||
|
||||
def __init__(self, auth_module: MultiFactorAuthModule,
|
||||
setup_schema: vol.Schema,
|
||||
user_id: str) -> None:
|
||||
"""Initialize the setup flow."""
|
||||
self._auth_module = auth_module
|
||||
self._setup_schema = setup_schema
|
||||
self._user_id = user_id
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: Optional[Dict[str, str]] = None) \
|
||||
-> Dict[str, Any]:
|
||||
"""Handle the first step of setup flow.
|
||||
|
||||
Return self.async_show_form(step_id='init') if user_input == None.
|
||||
Return self.async_create_entry(data={'result': result}) if finish.
|
||||
"""
|
||||
errors = {} # type: Dict[str, str]
|
||||
|
||||
if user_input:
|
||||
result = await self._auth_module.async_setup_user(
|
||||
self._user_id, user_input)
|
||||
return self.async_create_entry(
|
||||
title=self._auth_module.name,
|
||||
data={'result': result}
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id='init',
|
||||
data_schema=self._setup_schema,
|
||||
errors=errors
|
||||
)
|
||||
|
||||
|
||||
async def auth_mfa_module_from_config(
|
||||
hass: HomeAssistant, config: Dict[str, Any]) \
|
||||
-> Optional[MultiFactorAuthModule]:
|
||||
"""Initialize an auth module from a config."""
|
||||
module_name = config[CONF_TYPE]
|
||||
module = await _load_mfa_module(hass, module_name)
|
||||
|
||||
if module is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
config = module.CONFIG_SCHEMA(config) # type: ignore
|
||||
except vol.Invalid as err:
|
||||
_LOGGER.error('Invalid configuration for multi-factor module %s: %s',
|
||||
module_name, humanize_error(config, err))
|
||||
return None
|
||||
|
||||
return MULTI_FACTOR_AUTH_MODULES[module_name](hass, config) # type: ignore
|
||||
|
||||
|
||||
async def _load_mfa_module(hass: HomeAssistant, module_name: str) \
|
||||
-> Optional[types.ModuleType]:
|
||||
"""Load an mfa auth module."""
|
||||
module_path = 'homeassistant.auth.mfa_modules.{}'.format(module_name)
|
||||
|
||||
try:
|
||||
module = importlib.import_module(module_path)
|
||||
except ImportError:
|
||||
_LOGGER.warning('Unable to find %s', module_path)
|
||||
return None
|
||||
|
||||
if hass.config.skip_pip or not hasattr(module, 'REQUIREMENTS'):
|
||||
return module
|
||||
|
||||
processed = hass.data.get(DATA_REQS)
|
||||
if processed and module_name in processed:
|
||||
return module
|
||||
|
||||
processed = hass.data[DATA_REQS] = set()
|
||||
|
||||
# https://github.com/python/mypy/issues/1424
|
||||
req_success = await requirements.async_process_requirements(
|
||||
hass, module_path, module.REQUIREMENTS) # type: ignore
|
||||
|
||||
if not req_success:
|
||||
return None
|
||||
|
||||
processed.add(module_name)
|
||||
return module
|
||||
89
homeassistant/auth/mfa_modules/insecure_example.py
Normal file
89
homeassistant/auth/mfa_modules/insecure_example.py
Normal file
@@ -0,0 +1,89 @@
|
||||
"""Example auth module."""
|
||||
import logging
|
||||
from typing import Any, Dict
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import MultiFactorAuthModule, MULTI_FACTOR_AUTH_MODULES, \
|
||||
MULTI_FACTOR_AUTH_MODULE_SCHEMA, SetupFlow
|
||||
|
||||
CONFIG_SCHEMA = MULTI_FACTOR_AUTH_MODULE_SCHEMA.extend({
|
||||
vol.Required('data'): [vol.Schema({
|
||||
vol.Required('user_id'): str,
|
||||
vol.Required('pin'): str,
|
||||
})]
|
||||
}, extra=vol.PREVENT_EXTRA)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@MULTI_FACTOR_AUTH_MODULES.register('insecure_example')
|
||||
class InsecureExampleModule(MultiFactorAuthModule):
|
||||
"""Example auth module validate pin."""
|
||||
|
||||
DEFAULT_TITLE = 'Insecure Personal Identify Number'
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: Dict[str, Any]) -> None:
|
||||
"""Initialize the user data store."""
|
||||
super().__init__(hass, config)
|
||||
self._data = config['data']
|
||||
|
||||
@property
|
||||
def input_schema(self) -> vol.Schema:
|
||||
"""Validate login flow input data."""
|
||||
return vol.Schema({'pin': str})
|
||||
|
||||
@property
|
||||
def setup_schema(self) -> vol.Schema:
|
||||
"""Validate async_setup_user input data."""
|
||||
return vol.Schema({'pin': str})
|
||||
|
||||
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 SetupFlow(self, self.setup_schema, user_id)
|
||||
|
||||
async def async_setup_user(self, user_id: str, setup_data: Any) -> Any:
|
||||
"""Set up user to use mfa module."""
|
||||
# data shall has been validate in caller
|
||||
pin = setup_data['pin']
|
||||
|
||||
for data in self._data:
|
||||
if data['user_id'] == user_id:
|
||||
# already setup, override
|
||||
data['pin'] = pin
|
||||
return
|
||||
|
||||
self._data.append({'user_id': user_id, 'pin': pin})
|
||||
|
||||
async def async_depose_user(self, user_id: str) -> None:
|
||||
"""Remove user from mfa module."""
|
||||
found = None
|
||||
for data in self._data:
|
||||
if data['user_id'] == user_id:
|
||||
found = data
|
||||
break
|
||||
if found:
|
||||
self._data.remove(found)
|
||||
|
||||
async def async_is_user_setup(self, user_id: str) -> bool:
|
||||
"""Return whether user is setup."""
|
||||
for data in self._data:
|
||||
if data['user_id'] == user_id:
|
||||
return True
|
||||
return False
|
||||
|
||||
async def async_validation(
|
||||
self, user_id: str, user_input: Dict[str, Any]) -> bool:
|
||||
"""Return True if validation passed."""
|
||||
for data in self._data:
|
||||
if data['user_id'] == user_id:
|
||||
# user_input has been validate in caller
|
||||
if data['pin'] == user_input['pin']:
|
||||
return True
|
||||
|
||||
return False
|
||||
212
homeassistant/auth/mfa_modules/totp.py
Normal file
212
homeassistant/auth/mfa_modules/totp.py
Normal file
@@ -0,0 +1,212 @@
|
||||
"""Time-based One Time Password auth module."""
|
||||
import logging
|
||||
from io import BytesIO
|
||||
from typing import Any, Dict, Optional, Tuple # noqa: F401
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.auth.models import User
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import MultiFactorAuthModule, MULTI_FACTOR_AUTH_MODULES, \
|
||||
MULTI_FACTOR_AUTH_MODULE_SCHEMA, SetupFlow
|
||||
|
||||
REQUIREMENTS = ['pyotp==2.2.6', 'PyQRCode==1.2.1']
|
||||
|
||||
CONFIG_SCHEMA = MULTI_FACTOR_AUTH_MODULE_SCHEMA.extend({
|
||||
}, extra=vol.PREVENT_EXTRA)
|
||||
|
||||
STORAGE_VERSION = 1
|
||||
STORAGE_KEY = 'auth_module.totp'
|
||||
STORAGE_USERS = 'users'
|
||||
STORAGE_USER_ID = 'user_id'
|
||||
STORAGE_OTA_SECRET = 'ota_secret'
|
||||
|
||||
INPUT_FIELD_CODE = 'code'
|
||||
|
||||
DUMMY_SECRET = 'FPPTH34D4E3MI2HG'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _generate_qr_code(data: str) -> str:
|
||||
"""Generate a base64 PNG string represent QR Code image of data."""
|
||||
import pyqrcode
|
||||
|
||||
qr_code = pyqrcode.create(data)
|
||||
|
||||
with BytesIO() as buffer:
|
||||
qr_code.svg(file=buffer, scale=4)
|
||||
return '{}'.format(
|
||||
buffer.getvalue().decode("ascii").replace('\n', '')
|
||||
.replace('<?xml version="1.0" encoding="UTF-8"?>'
|
||||
'<svg xmlns="http://www.w3.org/2000/svg"', '<svg')
|
||||
)
|
||||
|
||||
|
||||
def _generate_secret_and_qr_code(username: str) -> Tuple[str, str, str]:
|
||||
"""Generate a secret, url, and QR code."""
|
||||
import pyotp
|
||||
|
||||
ota_secret = pyotp.random_base32()
|
||||
url = pyotp.totp.TOTP(ota_secret).provisioning_uri(
|
||||
username, issuer_name="Home Assistant")
|
||||
image = _generate_qr_code(url)
|
||||
return ota_secret, url, image
|
||||
|
||||
|
||||
@MULTI_FACTOR_AUTH_MODULES.register('totp')
|
||||
class TotpAuthModule(MultiFactorAuthModule):
|
||||
"""Auth module validate time-based one time password."""
|
||||
|
||||
DEFAULT_TITLE = 'Time-based One Time Password'
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: Dict[str, Any]) -> None:
|
||||
"""Initialize the user data store."""
|
||||
super().__init__(hass, config)
|
||||
self._users = None # type: Optional[Dict[str, str]]
|
||||
self._user_store = hass.helpers.storage.Store(
|
||||
STORAGE_VERSION, STORAGE_KEY)
|
||||
|
||||
@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._users = data.get(STORAGE_USERS, {})
|
||||
|
||||
async def _async_save(self) -> None:
|
||||
"""Save data."""
|
||||
await self._user_store.async_save({STORAGE_USERS: self._users})
|
||||
|
||||
def _add_ota_secret(self, user_id: str,
|
||||
secret: Optional[str] = None) -> str:
|
||||
"""Create a ota_secret for user."""
|
||||
import pyotp
|
||||
|
||||
ota_secret = secret or pyotp.random_base32() # type: str
|
||||
|
||||
self._users[user_id] = ota_secret # type: ignore
|
||||
return ota_secret
|
||||
|
||||
async def async_setup_flow(self, user_id: str) -> SetupFlow:
|
||||
"""Return a data entry flow handler for setup module.
|
||||
|
||||
Mfa module should extend SetupFlow
|
||||
"""
|
||||
user = await self.hass.auth.async_get_user(user_id) # type: ignore
|
||||
return TotpSetupFlow(self, self.input_schema, user)
|
||||
|
||||
async def async_setup_user(self, user_id: str, setup_data: Any) -> str:
|
||||
"""Set up auth module for user."""
|
||||
if self._users is None:
|
||||
await self._async_load()
|
||||
|
||||
result = await self.hass.async_add_executor_job(
|
||||
self._add_ota_secret, user_id, setup_data.get('secret'))
|
||||
|
||||
await self._async_save()
|
||||
return result
|
||||
|
||||
async def async_depose_user(self, user_id: str) -> None:
|
||||
"""Depose auth module for user."""
|
||||
if self._users is None:
|
||||
await self._async_load()
|
||||
|
||||
if self._users.pop(user_id, None): # type: ignore
|
||||
await self._async_save()
|
||||
|
||||
async def async_is_user_setup(self, user_id: str) -> bool:
|
||||
"""Return whether user is setup."""
|
||||
if self._users is None:
|
||||
await self._async_load()
|
||||
|
||||
return user_id in self._users # type: ignore
|
||||
|
||||
async def async_validation(
|
||||
self, user_id: str, user_input: Dict[str, Any]) -> bool:
|
||||
"""Return True if validation passed."""
|
||||
if self._users is None:
|
||||
await self._async_load()
|
||||
|
||||
# user_input has been validate in caller
|
||||
return await self.hass.async_add_executor_job(
|
||||
self._validate_2fa, user_id, user_input[INPUT_FIELD_CODE])
|
||||
|
||||
def _validate_2fa(self, user_id: str, code: str) -> bool:
|
||||
"""Validate two factor authentication code."""
|
||||
import pyotp
|
||||
|
||||
ota_secret = self._users.get(user_id) # type: ignore
|
||||
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)
|
||||
return False
|
||||
|
||||
return bool(pyotp.TOTP(ota_secret).verify(code))
|
||||
|
||||
|
||||
class TotpSetupFlow(SetupFlow):
|
||||
"""Handler for the setup flow."""
|
||||
|
||||
def __init__(self, auth_module: TotpAuthModule,
|
||||
setup_schema: vol.Schema,
|
||||
user: User) -> None:
|
||||
"""Initialize the setup flow."""
|
||||
super().__init__(auth_module, setup_schema, user.id)
|
||||
# to fix typing complaint
|
||||
self._auth_module = auth_module # type: TotpAuthModule
|
||||
self._user = user
|
||||
self._ota_secret = None # type: Optional[str]
|
||||
self._url = None # type Optional[str]
|
||||
self._image = None # type Optional[str]
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: Optional[Dict[str, str]] = None) \
|
||||
-> Dict[str, Any]:
|
||||
"""Handle the first step of setup flow.
|
||||
|
||||
Return self.async_show_form(step_id='init') if user_input == None.
|
||||
Return self.async_create_entry(data={'result': result}) if finish.
|
||||
"""
|
||||
import pyotp
|
||||
|
||||
errors = {} # type: Dict[str, str]
|
||||
|
||||
if user_input:
|
||||
verified = await self.hass.async_add_executor_job( # type: ignore
|
||||
pyotp.TOTP(self._ota_secret).verify, user_input['code'])
|
||||
if verified:
|
||||
result = await self._auth_module.async_setup_user(
|
||||
self._user_id, {'secret': self._ota_secret})
|
||||
return self.async_create_entry(
|
||||
title=self._auth_module.name,
|
||||
data={'result': result}
|
||||
)
|
||||
|
||||
errors['base'] = 'invalid_code'
|
||||
|
||||
else:
|
||||
hass = self._auth_module.hass
|
||||
self._ota_secret, self._url, self._image = \
|
||||
await hass.async_add_executor_job( # type: ignore
|
||||
_generate_secret_and_qr_code, str(self._user.name))
|
||||
|
||||
return self.async_show_form(
|
||||
step_id='init',
|
||||
data_schema=self._setup_schema,
|
||||
description_placeholders={
|
||||
'code': self._ota_secret,
|
||||
'url': self._url,
|
||||
'qr_code': self._image
|
||||
},
|
||||
errors=errors
|
||||
)
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Auth models."""
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, List, NamedTuple, Optional # noqa: F401
|
||||
import uuid
|
||||
|
||||
import attr
|
||||
@@ -14,17 +15,21 @@ from .util import generate_secret
|
||||
class User:
|
||||
"""A user."""
|
||||
|
||||
name = attr.ib(type=str)
|
||||
name = attr.ib(type=str) # type: Optional[str]
|
||||
id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex))
|
||||
is_owner = attr.ib(type=bool, default=False)
|
||||
is_active = attr.ib(type=bool, default=False)
|
||||
system_generated = attr.ib(type=bool, default=False)
|
||||
|
||||
# List of credentials of a user.
|
||||
credentials = attr.ib(type=list, default=attr.Factory(list), cmp=False)
|
||||
credentials = attr.ib(
|
||||
type=list, default=attr.Factory(list), cmp=False
|
||||
) # type: List[Credentials]
|
||||
|
||||
# Tokens associated with a user.
|
||||
refresh_tokens = attr.ib(type=dict, default=attr.Factory(dict), cmp=False)
|
||||
refresh_tokens = attr.ib(
|
||||
type=dict, default=attr.Factory(dict), cmp=False
|
||||
) # type: Dict[str, RefreshToken]
|
||||
|
||||
|
||||
@attr.s(slots=True)
|
||||
@@ -32,33 +37,15 @@ class RefreshToken:
|
||||
"""RefreshToken for a user to grant new access tokens."""
|
||||
|
||||
user = attr.ib(type=User)
|
||||
client_id = attr.ib(type=str)
|
||||
client_id = attr.ib(type=str) # type: Optional[str]
|
||||
id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex))
|
||||
created_at = attr.ib(type=datetime, default=attr.Factory(dt_util.utcnow))
|
||||
access_token_expiration = attr.ib(type=timedelta,
|
||||
default=ACCESS_TOKEN_EXPIRATION)
|
||||
token = attr.ib(type=str,
|
||||
default=attr.Factory(lambda: generate_secret(64)))
|
||||
access_tokens = attr.ib(type=list, default=attr.Factory(list), cmp=False)
|
||||
|
||||
|
||||
@attr.s(slots=True)
|
||||
class AccessToken:
|
||||
"""Access token to access the API.
|
||||
|
||||
These will only ever be stored in memory and not be persisted.
|
||||
"""
|
||||
|
||||
refresh_token = attr.ib(type=RefreshToken)
|
||||
created_at = attr.ib(type=datetime, default=attr.Factory(dt_util.utcnow))
|
||||
token = attr.ib(type=str,
|
||||
default=attr.Factory(generate_secret))
|
||||
|
||||
@property
|
||||
def expired(self):
|
||||
"""Return if this token has expired."""
|
||||
expires = self.created_at + self.refresh_token.access_token_expiration
|
||||
return dt_util.utcnow() > expires
|
||||
jwt_key = attr.ib(type=str,
|
||||
default=attr.Factory(lambda: generate_secret(64)))
|
||||
|
||||
|
||||
@attr.s(slots=True)
|
||||
@@ -66,10 +53,14 @@ class Credentials:
|
||||
"""Credentials for a user on an auth provider."""
|
||||
|
||||
auth_provider_type = attr.ib(type=str)
|
||||
auth_provider_id = attr.ib(type=str)
|
||||
auth_provider_id = attr.ib(type=str) # type: Optional[str]
|
||||
|
||||
# Allow the auth provider to store data to represent their auth.
|
||||
data = attr.ib(type=dict)
|
||||
|
||||
id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex))
|
||||
is_new = attr.ib(type=bool, default=True)
|
||||
|
||||
|
||||
UserMeta = NamedTuple("UserMeta",
|
||||
[('name', Optional[str]), ('is_active', bool)])
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
"""Auth providers for Home Assistant."""
|
||||
import importlib
|
||||
import logging
|
||||
import types
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import voluptuous as vol
|
||||
from voluptuous.humanize import humanize_error
|
||||
|
||||
from homeassistant import requirements
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import CONF_TYPE, CONF_NAME, CONF_ID
|
||||
from homeassistant import data_entry_flow, requirements
|
||||
from homeassistant.core import callback, HomeAssistant
|
||||
from homeassistant.const import CONF_ID, CONF_NAME, CONF_TYPE
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.util.decorator import Registry
|
||||
|
||||
from homeassistant.auth.models import Credentials
|
||||
from ..auth_store import AuthStore
|
||||
from ..models import Credentials, User, UserMeta # noqa: F401
|
||||
from ..mfa_modules import SESSION_EXPIRATION
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
DATA_REQS = 'auth_prov_reqs_processed'
|
||||
@@ -25,7 +30,87 @@ AUTH_PROVIDER_SCHEMA = vol.Schema({
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
async def auth_provider_from_config(hass, store, config):
|
||||
class AuthProvider:
|
||||
"""Provider of user authentication."""
|
||||
|
||||
DEFAULT_TITLE = 'Unnamed auth provider'
|
||||
|
||||
def __init__(self, hass: HomeAssistant, store: AuthStore,
|
||||
config: Dict[str, Any]) -> None:
|
||||
"""Initialize an auth provider."""
|
||||
self.hass = hass
|
||||
self.store = store
|
||||
self.config = config
|
||||
|
||||
@property
|
||||
def id(self) -> Optional[str]: # pylint: disable=invalid-name
|
||||
"""Return id of the auth provider.
|
||||
|
||||
Optional, can be None.
|
||||
"""
|
||||
return self.config.get(CONF_ID)
|
||||
|
||||
@property
|
||||
def type(self) -> str:
|
||||
"""Return type of the provider."""
|
||||
return self.config[CONF_TYPE] # type: ignore
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the name of the auth provider."""
|
||||
return self.config.get(CONF_NAME, self.DEFAULT_TITLE)
|
||||
|
||||
@property
|
||||
def support_mfa(self) -> bool:
|
||||
"""Return whether multi-factor auth supported by the auth provider."""
|
||||
return True
|
||||
|
||||
async def async_credentials(self) -> List[Credentials]:
|
||||
"""Return all credentials of this provider."""
|
||||
users = await self.store.async_get_users()
|
||||
return [
|
||||
credentials
|
||||
for user in users
|
||||
for credentials in user.credentials
|
||||
if (credentials.auth_provider_type == self.type and
|
||||
credentials.auth_provider_id == self.id)
|
||||
]
|
||||
|
||||
@callback
|
||||
def async_create_credentials(self, data: Dict[str, str]) -> Credentials:
|
||||
"""Create credentials."""
|
||||
return Credentials(
|
||||
auth_provider_type=self.type,
|
||||
auth_provider_id=self.id,
|
||||
data=data,
|
||||
)
|
||||
|
||||
# Implement by extending class
|
||||
|
||||
async def async_login_flow(self, context: Optional[Dict]) -> 'LoginFlow':
|
||||
"""Return the data flow for logging in with auth provider.
|
||||
|
||||
Auth provider should extend LoginFlow and return an instance.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
async def async_get_or_create_credentials(
|
||||
self, flow_result: Dict[str, str]) -> Credentials:
|
||||
"""Get credentials based on the flow result."""
|
||||
raise NotImplementedError
|
||||
|
||||
async def async_user_meta_for_credentials(
|
||||
self, credentials: Credentials) -> UserMeta:
|
||||
"""Return extra user metadata for credentials.
|
||||
|
||||
Will be used to populate info when creating a new user.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
async def auth_provider_from_config(
|
||||
hass: HomeAssistant, store: AuthStore,
|
||||
config: Dict[str, Any]) -> Optional[AuthProvider]:
|
||||
"""Initialize an auth provider from a config."""
|
||||
provider_name = config[CONF_TYPE]
|
||||
module = await load_auth_provider_module(hass, provider_name)
|
||||
@@ -34,16 +119,17 @@ async def auth_provider_from_config(hass, store, config):
|
||||
return None
|
||||
|
||||
try:
|
||||
config = module.CONFIG_SCHEMA(config)
|
||||
config = module.CONFIG_SCHEMA(config) # type: ignore
|
||||
except vol.Invalid as err:
|
||||
_LOGGER.error('Invalid configuration for auth provider %s: %s',
|
||||
provider_name, humanize_error(config, err))
|
||||
return None
|
||||
|
||||
return AUTH_PROVIDERS[provider_name](hass, store, config)
|
||||
return AUTH_PROVIDERS[provider_name](hass, store, config) # type: ignore
|
||||
|
||||
|
||||
async def load_auth_provider_module(hass, provider):
|
||||
async def load_auth_provider_module(
|
||||
hass: HomeAssistant, provider: str) -> Optional[types.ModuleType]:
|
||||
"""Load an auth provider."""
|
||||
try:
|
||||
module = importlib.import_module(
|
||||
@@ -62,8 +148,10 @@ async def load_auth_provider_module(hass, provider):
|
||||
elif provider in processed:
|
||||
return module
|
||||
|
||||
# https://github.com/python/mypy/issues/1424
|
||||
reqs = module.REQUIREMENTS # type: ignore
|
||||
req_success = await requirements.async_process_requirements(
|
||||
hass, 'auth provider {}'.format(provider), module.REQUIREMENTS)
|
||||
hass, 'auth provider {}'.format(provider), reqs)
|
||||
|
||||
if not req_success:
|
||||
return None
|
||||
@@ -72,72 +160,88 @@ async def load_auth_provider_module(hass, provider):
|
||||
return module
|
||||
|
||||
|
||||
class AuthProvider:
|
||||
"""Provider of user authentication."""
|
||||
class LoginFlow(data_entry_flow.FlowHandler):
|
||||
"""Handler for the login flow."""
|
||||
|
||||
DEFAULT_TITLE = 'Unnamed auth provider'
|
||||
def __init__(self, auth_provider: AuthProvider) -> None:
|
||||
"""Initialize the login flow."""
|
||||
self._auth_provider = auth_provider
|
||||
self._auth_module_id = None # type: Optional[str]
|
||||
self._auth_manager = auth_provider.hass.auth # type: ignore
|
||||
self.available_mfa_modules = {} # type: Dict[str, str]
|
||||
self.created_at = dt_util.utcnow()
|
||||
self.user = None # type: Optional[User]
|
||||
|
||||
def __init__(self, hass, store, config):
|
||||
"""Initialize an auth provider."""
|
||||
self.hass = hass
|
||||
self.store = store
|
||||
self.config = config
|
||||
async def async_step_init(
|
||||
self, user_input: Optional[Dict[str, str]] = None) \
|
||||
-> Dict[str, Any]:
|
||||
"""Handle the first step of login flow.
|
||||
|
||||
@property
|
||||
def id(self): # pylint: disable=invalid-name
|
||||
"""Return id of the auth provider.
|
||||
|
||||
Optional, can be None.
|
||||
Return self.async_show_form(step_id='init') if user_input == None.
|
||||
Return await self.async_finish(flow_result) if login init step pass.
|
||||
"""
|
||||
return self.config.get(CONF_ID)
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
"""Return type of the provider."""
|
||||
return self.config[CONF_TYPE]
|
||||
async def async_step_select_mfa_module(
|
||||
self, user_input: Optional[Dict[str, str]] = None) \
|
||||
-> Dict[str, Any]:
|
||||
"""Handle the step of select mfa module."""
|
||||
errors = {}
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the auth provider."""
|
||||
return self.config.get(CONF_NAME, self.DEFAULT_TITLE)
|
||||
if user_input is not None:
|
||||
auth_module = user_input.get('multi_factor_auth_module')
|
||||
if auth_module in self.available_mfa_modules:
|
||||
self._auth_module_id = auth_module
|
||||
return await self.async_step_mfa()
|
||||
errors['base'] = 'invalid_auth_module'
|
||||
|
||||
async def async_credentials(self):
|
||||
"""Return all credentials of this provider."""
|
||||
users = await self.store.async_get_users()
|
||||
return [
|
||||
credentials
|
||||
for user in users
|
||||
for credentials in user.credentials
|
||||
if (credentials.auth_provider_type == self.type and
|
||||
credentials.auth_provider_id == self.id)
|
||||
]
|
||||
if len(self.available_mfa_modules) == 1:
|
||||
self._auth_module_id = list(self.available_mfa_modules.keys())[0]
|
||||
return await self.async_step_mfa()
|
||||
|
||||
@callback
|
||||
def async_create_credentials(self, data):
|
||||
"""Create credentials."""
|
||||
return Credentials(
|
||||
auth_provider_type=self.type,
|
||||
auth_provider_id=self.id,
|
||||
data=data,
|
||||
return self.async_show_form(
|
||||
step_id='select_mfa_module',
|
||||
data_schema=vol.Schema({
|
||||
'multi_factor_auth_module': vol.In(self.available_mfa_modules)
|
||||
}),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
# Implement by extending class
|
||||
async def async_step_mfa(
|
||||
self, user_input: Optional[Dict[str, str]] = None) \
|
||||
-> Dict[str, Any]:
|
||||
"""Handle the step of mfa validation."""
|
||||
errors = {}
|
||||
|
||||
async def async_credential_flow(self):
|
||||
"""Return the data flow for logging in with auth provider."""
|
||||
raise NotImplementedError
|
||||
auth_module = self._auth_manager.get_auth_mfa_module(
|
||||
self._auth_module_id)
|
||||
if auth_module is None:
|
||||
# Given an invalid input to async_step_select_mfa_module
|
||||
# will show invalid_auth_module error
|
||||
return await self.async_step_select_mfa_module(user_input={})
|
||||
|
||||
async def async_get_or_create_credentials(self, flow_result):
|
||||
"""Get credentials based on the flow result."""
|
||||
raise NotImplementedError
|
||||
if user_input is not None:
|
||||
expires = self.created_at + SESSION_EXPIRATION
|
||||
if dt_util.utcnow() > expires:
|
||||
errors['base'] = 'login_expired'
|
||||
else:
|
||||
result = await auth_module.async_validation(
|
||||
self.user.id, user_input) # type: ignore
|
||||
if not result:
|
||||
errors['base'] = 'invalid_auth'
|
||||
|
||||
async def async_user_meta_for_credentials(self, credentials):
|
||||
"""Return extra user metadata for credentials.
|
||||
if not errors:
|
||||
return await self.async_finish(self.user)
|
||||
|
||||
Will be used to populate info when creating a new user.
|
||||
return self.async_show_form(
|
||||
step_id='mfa',
|
||||
data_schema=auth_module.input_schema,
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
Values to populate:
|
||||
- name: string
|
||||
- is_active: boolean
|
||||
"""
|
||||
return {}
|
||||
async def async_finish(self, flow_result: Any) -> Dict:
|
||||
"""Handle the pass of login flow."""
|
||||
return self.async_create_entry(
|
||||
title=self._auth_provider.name,
|
||||
data=flow_result
|
||||
)
|
||||
|
||||
@@ -3,24 +3,27 @@ import base64
|
||||
from collections import OrderedDict
|
||||
import hashlib
|
||||
import hmac
|
||||
from typing import Dict # noqa: F401 pylint: disable=unused-import
|
||||
from typing import Any, Dict, List, Optional, cast
|
||||
|
||||
import bcrypt
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import data_entry_flow
|
||||
from homeassistant.const import CONF_ID
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.core import callback, HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.util.async_ import run_coroutine_threadsafe
|
||||
|
||||
from homeassistant.auth.util import generate_secret
|
||||
from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, LoginFlow
|
||||
|
||||
from ..models import Credentials, UserMeta
|
||||
from ..util import generate_secret
|
||||
|
||||
from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS
|
||||
|
||||
STORAGE_VERSION = 1
|
||||
STORAGE_KEY = 'auth_provider.homeassistant'
|
||||
|
||||
|
||||
def _disallow_id(conf):
|
||||
def _disallow_id(conf: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Disallow ID in config."""
|
||||
if CONF_ID in conf:
|
||||
raise vol.Invalid(
|
||||
@@ -46,13 +49,13 @@ class InvalidUser(HomeAssistantError):
|
||||
class Data:
|
||||
"""Hold the user data."""
|
||||
|
||||
def __init__(self, hass):
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
"""Initialize the user data store."""
|
||||
self.hass = hass
|
||||
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
|
||||
self._data = None
|
||||
self._data = None # type: Optional[Dict[str, Any]]
|
||||
|
||||
async def async_load(self):
|
||||
async def async_load(self) -> None:
|
||||
"""Load stored data."""
|
||||
data = await self._store.async_load()
|
||||
|
||||
@@ -65,37 +68,69 @@ class Data:
|
||||
self._data = data
|
||||
|
||||
@property
|
||||
def users(self):
|
||||
def users(self) -> List[Dict[str, str]]:
|
||||
"""Return users."""
|
||||
return self._data['users']
|
||||
return self._data['users'] # type: ignore
|
||||
|
||||
def validate_login(self, username: str, password: str) -> None:
|
||||
"""Validate a username and password.
|
||||
|
||||
Raises InvalidAuth if auth invalid.
|
||||
"""
|
||||
hashed = self.hash_password(password)
|
||||
|
||||
dummy = b'$2b$12$CiuFGszHx9eNHxPuQcwBWez4CwDTOcLTX5CbOpV6gef2nYuXkY7BO'
|
||||
found = None
|
||||
|
||||
# Compare all users to avoid timing attacks.
|
||||
for user in self._data['users']:
|
||||
for user in self.users:
|
||||
if username == user['username']:
|
||||
found = user
|
||||
|
||||
if found is None:
|
||||
# Do one more compare to make timing the same as if user was found.
|
||||
hmac.compare_digest(hashed, hashed)
|
||||
# check a hash to make timing the same as if user was found
|
||||
bcrypt.checkpw(b'foo',
|
||||
dummy)
|
||||
raise InvalidAuth
|
||||
|
||||
if not hmac.compare_digest(hashed,
|
||||
base64.b64decode(found['password'])):
|
||||
user_hash = base64.b64decode(found['password'])
|
||||
|
||||
# if the hash is not a bcrypt hash...
|
||||
# provide a transparant upgrade for old pbkdf2 hash format
|
||||
if not (user_hash.startswith(b'$2a$')
|
||||
or user_hash.startswith(b'$2b$')
|
||||
or user_hash.startswith(b'$2x$')
|
||||
or user_hash.startswith(b'$2y$')):
|
||||
# IMPORTANT! validate the login, bail if invalid
|
||||
hashed = self.legacy_hash_password(password)
|
||||
if not hmac.compare_digest(hashed, user_hash):
|
||||
raise InvalidAuth
|
||||
# then re-hash the valid password with bcrypt
|
||||
self.change_password(found['username'], password)
|
||||
run_coroutine_threadsafe(
|
||||
self.async_save(), self.hass.loop
|
||||
).result()
|
||||
user_hash = base64.b64decode(found['password'])
|
||||
|
||||
# bcrypt.checkpw is timing-safe
|
||||
if not bcrypt.checkpw(password.encode(),
|
||||
user_hash):
|
||||
raise InvalidAuth
|
||||
|
||||
def legacy_hash_password(self, password: str,
|
||||
for_storage: bool = False) -> bytes:
|
||||
"""LEGACY password encoding."""
|
||||
# We're no longer storing salts in data, but if one exists we
|
||||
# should be able to retrieve it.
|
||||
salt = self._data['salt'].encode() # type: ignore
|
||||
hashed = hashlib.pbkdf2_hmac('sha512', password.encode(), salt, 100000)
|
||||
if for_storage:
|
||||
hashed = base64.b64encode(hashed)
|
||||
return hashed
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
def hash_password(self, password: str, for_storage: bool = False) -> bytes:
|
||||
"""Encode a password."""
|
||||
hashed = hashlib.pbkdf2_hmac(
|
||||
'sha512', password.encode(), self._data['salt'].encode(), 100000)
|
||||
hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12)) \
|
||||
# type: bytes
|
||||
if for_storage:
|
||||
hashed = base64.b64encode(hashed)
|
||||
return hashed
|
||||
@@ -137,7 +172,7 @@ class Data:
|
||||
else:
|
||||
raise InvalidUser
|
||||
|
||||
async def async_save(self):
|
||||
async def async_save(self) -> None:
|
||||
"""Save data."""
|
||||
await self._store.async_save(self._data)
|
||||
|
||||
@@ -150,7 +185,7 @@ class HassAuthProvider(AuthProvider):
|
||||
|
||||
data = None
|
||||
|
||||
async def async_initialize(self):
|
||||
async def async_initialize(self) -> None:
|
||||
"""Initialize the auth provider."""
|
||||
if self.data is not None:
|
||||
return
|
||||
@@ -158,19 +193,22 @@ class HassAuthProvider(AuthProvider):
|
||||
self.data = Data(self.hass)
|
||||
await self.data.async_load()
|
||||
|
||||
async def async_credential_flow(self):
|
||||
async def async_login_flow(
|
||||
self, context: Optional[Dict]) -> LoginFlow:
|
||||
"""Return a flow to login."""
|
||||
return LoginFlow(self)
|
||||
return HassLoginFlow(self)
|
||||
|
||||
async def async_validate_login(self, username: str, password: str):
|
||||
"""Helper to validate a username and password."""
|
||||
async def async_validate_login(self, username: str, password: str) -> None:
|
||||
"""Validate a username and password."""
|
||||
if self.data is None:
|
||||
await self.async_initialize()
|
||||
assert self.data is not None
|
||||
|
||||
await self.hass.async_add_executor_job(
|
||||
self.data.validate_login, username, password)
|
||||
|
||||
async def async_get_or_create_credentials(self, flow_result):
|
||||
async def async_get_or_create_credentials(
|
||||
self, flow_result: Dict[str, str]) -> Credentials:
|
||||
"""Get credentials based on the flow result."""
|
||||
username = flow_result['username']
|
||||
|
||||
@@ -183,17 +221,17 @@ class HassAuthProvider(AuthProvider):
|
||||
'username': username
|
||||
})
|
||||
|
||||
async def async_user_meta_for_credentials(self, credentials):
|
||||
async def async_user_meta_for_credentials(
|
||||
self, credentials: Credentials) -> UserMeta:
|
||||
"""Get extra info for this credential."""
|
||||
return {
|
||||
'name': credentials.data['username'],
|
||||
'is_active': True,
|
||||
}
|
||||
return UserMeta(name=credentials.data['username'], is_active=True)
|
||||
|
||||
async def async_will_remove_credentials(self, credentials):
|
||||
async def async_will_remove_credentials(
|
||||
self, credentials: Credentials) -> None:
|
||||
"""When credentials get removed, also remove the auth."""
|
||||
if self.data is None:
|
||||
await self.async_initialize()
|
||||
assert self.data is not None
|
||||
|
||||
try:
|
||||
self.data.async_remove_auth(credentials.data['username'])
|
||||
@@ -203,29 +241,26 @@ class HassAuthProvider(AuthProvider):
|
||||
pass
|
||||
|
||||
|
||||
class LoginFlow(data_entry_flow.FlowHandler):
|
||||
class HassLoginFlow(LoginFlow):
|
||||
"""Handler for the login flow."""
|
||||
|
||||
def __init__(self, auth_provider):
|
||||
"""Initialize the login flow."""
|
||||
self._auth_provider = auth_provider
|
||||
|
||||
async def async_step_init(self, user_input=None):
|
||||
async def async_step_init(
|
||||
self, user_input: Optional[Dict[str, str]] = None) \
|
||||
-> Dict[str, Any]:
|
||||
"""Handle the step of the form."""
|
||||
errors = {}
|
||||
|
||||
if user_input is not None:
|
||||
try:
|
||||
await self._auth_provider.async_validate_login(
|
||||
user_input['username'], user_input['password'])
|
||||
await cast(HassAuthProvider, self._auth_provider)\
|
||||
.async_validate_login(user_input['username'],
|
||||
user_input['password'])
|
||||
except InvalidAuth:
|
||||
errors['base'] = 'invalid_auth'
|
||||
|
||||
if not errors:
|
||||
return self.async_create_entry(
|
||||
title=self._auth_provider.name,
|
||||
data=user_input
|
||||
)
|
||||
user_input.pop('password')
|
||||
return await self.async_finish(user_input)
|
||||
|
||||
schema = OrderedDict() # type: Dict[str, type]
|
||||
schema['username'] = str
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
"""Example auth provider."""
|
||||
from collections import OrderedDict
|
||||
import hmac
|
||||
from typing import Any, Dict, Optional, cast
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant import data_entry_flow
|
||||
from homeassistant.core import callback
|
||||
|
||||
from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS
|
||||
from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, LoginFlow
|
||||
from ..models import Credentials, UserMeta
|
||||
|
||||
|
||||
USER_SCHEMA = vol.Schema({
|
||||
@@ -31,13 +32,13 @@ class InvalidAuthError(HomeAssistantError):
|
||||
class ExampleAuthProvider(AuthProvider):
|
||||
"""Example auth provider based on hardcoded usernames and passwords."""
|
||||
|
||||
async def async_credential_flow(self):
|
||||
async def async_login_flow(self, context: Optional[Dict]) -> LoginFlow:
|
||||
"""Return a flow to login."""
|
||||
return LoginFlow(self)
|
||||
return ExampleLoginFlow(self)
|
||||
|
||||
@callback
|
||||
def async_validate_login(self, username, password):
|
||||
"""Helper to validate a username and password."""
|
||||
def async_validate_login(self, username: str, password: str) -> None:
|
||||
"""Validate a username and password."""
|
||||
user = None
|
||||
|
||||
# Compare all users to avoid timing attacks.
|
||||
@@ -56,7 +57,8 @@ class ExampleAuthProvider(AuthProvider):
|
||||
password.encode('utf-8')):
|
||||
raise InvalidAuthError
|
||||
|
||||
async def async_get_or_create_credentials(self, flow_result):
|
||||
async def async_get_or_create_credentials(
|
||||
self, flow_result: Dict[str, str]) -> Credentials:
|
||||
"""Get credentials based on the flow result."""
|
||||
username = flow_result['username']
|
||||
|
||||
@@ -69,49 +71,45 @@ class ExampleAuthProvider(AuthProvider):
|
||||
'username': username
|
||||
})
|
||||
|
||||
async def async_user_meta_for_credentials(self, credentials):
|
||||
async def async_user_meta_for_credentials(
|
||||
self, credentials: Credentials) -> UserMeta:
|
||||
"""Return extra user metadata for credentials.
|
||||
|
||||
Will be used to populate info when creating a new user.
|
||||
"""
|
||||
username = credentials.data['username']
|
||||
info = {
|
||||
'is_active': True,
|
||||
}
|
||||
name = None
|
||||
|
||||
for user in self.config['users']:
|
||||
if user['username'] == username:
|
||||
info['name'] = user.get('name')
|
||||
name = user.get('name')
|
||||
break
|
||||
|
||||
return info
|
||||
return UserMeta(name=name, is_active=True)
|
||||
|
||||
|
||||
class LoginFlow(data_entry_flow.FlowHandler):
|
||||
class ExampleLoginFlow(LoginFlow):
|
||||
"""Handler for the login flow."""
|
||||
|
||||
def __init__(self, auth_provider):
|
||||
"""Initialize the login flow."""
|
||||
self._auth_provider = auth_provider
|
||||
|
||||
async def async_step_init(self, user_input=None):
|
||||
async def async_step_init(
|
||||
self, user_input: Optional[Dict[str, str]] = None) \
|
||||
-> Dict[str, Any]:
|
||||
"""Handle the step of the form."""
|
||||
errors = {}
|
||||
|
||||
if user_input is not None:
|
||||
try:
|
||||
self._auth_provider.async_validate_login(
|
||||
user_input['username'], user_input['password'])
|
||||
cast(ExampleAuthProvider, self._auth_provider)\
|
||||
.async_validate_login(user_input['username'],
|
||||
user_input['password'])
|
||||
except InvalidAuthError:
|
||||
errors['base'] = 'invalid_auth'
|
||||
|
||||
if not errors:
|
||||
return self.async_create_entry(
|
||||
title=self._auth_provider.name,
|
||||
data=user_input
|
||||
)
|
||||
user_input.pop('password')
|
||||
return await self.async_finish(user_input)
|
||||
|
||||
schema = OrderedDict()
|
||||
schema = OrderedDict() # type: Dict[str, type]
|
||||
schema['username'] = str
|
||||
schema['password'] = str
|
||||
|
||||
|
||||
@@ -3,16 +3,17 @@ Support Legacy API password auth provider.
|
||||
|
||||
It will be removed when auth system production ready
|
||||
"""
|
||||
from collections import OrderedDict
|
||||
import hmac
|
||||
from typing import Any, Dict, Optional, cast
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant import data_entry_flow
|
||||
from homeassistant.components.http import HomeAssistantHTTP # noqa: F401
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS
|
||||
from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, LoginFlow
|
||||
from ..models import Credentials, UserMeta
|
||||
|
||||
|
||||
USER_SCHEMA = vol.Schema({
|
||||
@@ -36,25 +37,21 @@ class LegacyApiPasswordAuthProvider(AuthProvider):
|
||||
|
||||
DEFAULT_TITLE = 'Legacy API Password'
|
||||
|
||||
async def async_credential_flow(self):
|
||||
async def async_login_flow(self, context: Optional[Dict]) -> LoginFlow:
|
||||
"""Return a flow to login."""
|
||||
return LoginFlow(self)
|
||||
return LegacyLoginFlow(self)
|
||||
|
||||
@callback
|
||||
def async_validate_login(self, password):
|
||||
"""Helper to validate a username and password."""
|
||||
if not hasattr(self.hass, 'http'):
|
||||
raise ValueError('http component is not loaded')
|
||||
def async_validate_login(self, password: str) -> None:
|
||||
"""Validate a username and password."""
|
||||
hass_http = getattr(self.hass, 'http', None) # type: HomeAssistantHTTP
|
||||
|
||||
if self.hass.http.api_password is None:
|
||||
raise ValueError('http component is not configured using'
|
||||
' api_password')
|
||||
|
||||
if not hmac.compare_digest(self.hass.http.api_password.encode('utf-8'),
|
||||
if not hmac.compare_digest(hass_http.api_password.encode('utf-8'),
|
||||
password.encode('utf-8')):
|
||||
raise InvalidAuthError
|
||||
|
||||
async def async_get_or_create_credentials(self, flow_result):
|
||||
async def async_get_or_create_credentials(
|
||||
self, flow_result: Dict[str, str]) -> Credentials:
|
||||
"""Return LEGACY_USER always."""
|
||||
for credential in await self.async_credentials():
|
||||
if credential.data['username'] == LEGACY_USER:
|
||||
@@ -64,47 +61,43 @@ class LegacyApiPasswordAuthProvider(AuthProvider):
|
||||
'username': LEGACY_USER
|
||||
})
|
||||
|
||||
async def async_user_meta_for_credentials(self, credentials):
|
||||
async def async_user_meta_for_credentials(
|
||||
self, credentials: Credentials) -> UserMeta:
|
||||
"""
|
||||
Set name as LEGACY_USER always.
|
||||
|
||||
Will be used to populate info when creating a new user.
|
||||
"""
|
||||
return {
|
||||
'name': LEGACY_USER,
|
||||
'is_active': True,
|
||||
}
|
||||
return UserMeta(name=LEGACY_USER, is_active=True)
|
||||
|
||||
|
||||
class LoginFlow(data_entry_flow.FlowHandler):
|
||||
class LegacyLoginFlow(LoginFlow):
|
||||
"""Handler for the login flow."""
|
||||
|
||||
def __init__(self, auth_provider):
|
||||
"""Initialize the login flow."""
|
||||
self._auth_provider = auth_provider
|
||||
|
||||
async def async_step_init(self, user_input=None):
|
||||
async def async_step_init(
|
||||
self, user_input: Optional[Dict[str, str]] = None) \
|
||||
-> Dict[str, Any]:
|
||||
"""Handle the step of the form."""
|
||||
errors = {}
|
||||
|
||||
hass_http = getattr(self.hass, 'http', None)
|
||||
if hass_http is None or not hass_http.api_password:
|
||||
return self.async_abort(
|
||||
reason='no_api_password_set'
|
||||
)
|
||||
|
||||
if user_input is not None:
|
||||
try:
|
||||
self._auth_provider.async_validate_login(
|
||||
user_input['password'])
|
||||
cast(LegacyApiPasswordAuthProvider, self._auth_provider)\
|
||||
.async_validate_login(user_input['password'])
|
||||
except InvalidAuthError:
|
||||
errors['base'] = 'invalid_auth'
|
||||
|
||||
if not errors:
|
||||
return self.async_create_entry(
|
||||
title=self._auth_provider.name,
|
||||
data={}
|
||||
)
|
||||
|
||||
schema = OrderedDict()
|
||||
schema['password'] = str
|
||||
return await self.async_finish({})
|
||||
|
||||
return self.async_show_form(
|
||||
step_id='init',
|
||||
data_schema=vol.Schema(schema),
|
||||
data_schema=vol.Schema({'password': str}),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
141
homeassistant/auth/providers/trusted_networks.py
Normal file
141
homeassistant/auth/providers/trusted_networks.py
Normal file
@@ -0,0 +1,141 @@
|
||||
"""Trusted Networks auth provider.
|
||||
|
||||
It shows list of users if access from trusted network.
|
||||
Abort login flow if not access from trusted network.
|
||||
"""
|
||||
from typing import Any, Dict, Optional, cast
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.http import HomeAssistantHTTP # noqa: F401
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, LoginFlow
|
||||
from ..models import Credentials, UserMeta
|
||||
|
||||
CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend({
|
||||
}, extra=vol.PREVENT_EXTRA)
|
||||
|
||||
|
||||
class InvalidAuthError(HomeAssistantError):
|
||||
"""Raised when try to access from untrusted networks."""
|
||||
|
||||
|
||||
class InvalidUserError(HomeAssistantError):
|
||||
"""Raised when try to login as invalid user."""
|
||||
|
||||
|
||||
@AUTH_PROVIDERS.register('trusted_networks')
|
||||
class TrustedNetworksAuthProvider(AuthProvider):
|
||||
"""Trusted Networks auth provider.
|
||||
|
||||
Allow passwordless access from trusted network.
|
||||
"""
|
||||
|
||||
DEFAULT_TITLE = 'Trusted Networks'
|
||||
|
||||
@property
|
||||
def support_mfa(self) -> bool:
|
||||
"""Trusted Networks auth provider does not support MFA."""
|
||||
return False
|
||||
|
||||
async def async_login_flow(self, context: Optional[Dict]) -> LoginFlow:
|
||||
"""Return a flow to login."""
|
||||
assert context is not None
|
||||
users = await self.store.async_get_users()
|
||||
available_users = {user.id: user.name
|
||||
for user in users
|
||||
if not user.system_generated and user.is_active}
|
||||
|
||||
return TrustedNetworksLoginFlow(
|
||||
self, cast(str, context.get('ip_address')), available_users)
|
||||
|
||||
async def async_get_or_create_credentials(
|
||||
self, flow_result: Dict[str, str]) -> Credentials:
|
||||
"""Get credentials based on the flow result."""
|
||||
user_id = flow_result['user']
|
||||
|
||||
users = await self.store.async_get_users()
|
||||
for user in users:
|
||||
if (not user.system_generated and
|
||||
user.is_active and
|
||||
user.id == user_id):
|
||||
for credential in await self.async_credentials():
|
||||
if credential.data['user_id'] == user_id:
|
||||
return credential
|
||||
cred = self.async_create_credentials({'user_id': user_id})
|
||||
await self.store.async_link_user(user, cred)
|
||||
return cred
|
||||
|
||||
# We only allow login as exist user
|
||||
raise InvalidUserError
|
||||
|
||||
async def async_user_meta_for_credentials(
|
||||
self, credentials: Credentials) -> UserMeta:
|
||||
"""Return extra user metadata for credentials.
|
||||
|
||||
Trusted network auth provider should never create new user.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@callback
|
||||
def async_validate_access(self, ip_address: str) -> None:
|
||||
"""Make sure the access from trusted networks.
|
||||
|
||||
Raise InvalidAuthError if not.
|
||||
Raise InvalidAuthError if trusted_networks is not configured.
|
||||
"""
|
||||
hass_http = getattr(self.hass, 'http', None) # type: HomeAssistantHTTP
|
||||
|
||||
if not hass_http or not hass_http.trusted_networks:
|
||||
raise InvalidAuthError('trusted_networks is not configured')
|
||||
|
||||
if not any(ip_address in trusted_network for trusted_network
|
||||
in hass_http.trusted_networks):
|
||||
raise InvalidAuthError('Not in trusted_networks')
|
||||
|
||||
|
||||
class TrustedNetworksLoginFlow(LoginFlow):
|
||||
"""Handler for the login flow."""
|
||||
|
||||
def __init__(self, auth_provider: TrustedNetworksAuthProvider,
|
||||
ip_address: str, available_users: Dict[str, Optional[str]]) \
|
||||
-> None:
|
||||
"""Initialize the login flow."""
|
||||
super().__init__(auth_provider)
|
||||
self._available_users = available_users
|
||||
self._ip_address = ip_address
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: Optional[Dict[str, str]] = None) \
|
||||
-> Dict[str, Any]:
|
||||
"""Handle the step of the form."""
|
||||
errors = {}
|
||||
try:
|
||||
cast(TrustedNetworksAuthProvider, self._auth_provider)\
|
||||
.async_validate_access(self._ip_address)
|
||||
|
||||
except InvalidAuthError:
|
||||
errors['base'] = 'invalid_auth'
|
||||
return self.async_show_form(
|
||||
step_id='init',
|
||||
data_schema=None,
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
if user_input is not None:
|
||||
user_id = user_input['user']
|
||||
if user_id not in self._available_users:
|
||||
errors['base'] = 'invalid_auth'
|
||||
|
||||
if not errors:
|
||||
return await self.async_finish(user_input)
|
||||
|
||||
schema = {'user': vol.In(self._available_users)}
|
||||
|
||||
return self.async_show_form(
|
||||
step_id='init',
|
||||
data_schema=vol.Schema(schema),
|
||||
errors=errors,
|
||||
)
|
||||
@@ -87,9 +87,13 @@ async def async_from_config_dict(config: Dict[str, Any],
|
||||
log_no_color)
|
||||
|
||||
core_config = config.get(core.DOMAIN, {})
|
||||
has_api_password = bool((config.get('http') or {}).get('api_password'))
|
||||
has_trusted_networks = bool((config.get('http') or {})
|
||||
.get('trusted_networks'))
|
||||
|
||||
try:
|
||||
await conf_util.async_process_ha_core_config(hass, core_config)
|
||||
await conf_util.async_process_ha_core_config(
|
||||
hass, core_config, has_api_password, has_trusted_networks)
|
||||
except vol.Invalid as ex:
|
||||
conf_util.async_log_exception(ex, 'homeassistant', core_config, hass)
|
||||
return None
|
||||
@@ -307,7 +311,7 @@ def async_enable_logging(hass: core.HomeAssistant,
|
||||
hass.data[DATA_LOGGING] = err_log_path
|
||||
else:
|
||||
_LOGGER.error(
|
||||
"Unable to setup error log %s (access denied)", err_log_path)
|
||||
"Unable to set up error log %s (access denied)", err_log_path)
|
||||
|
||||
|
||||
async def async_mount_local_lib_path(config_dir: str) -> str:
|
||||
|
||||
@@ -10,6 +10,7 @@ Component design guidelines:
|
||||
import asyncio
|
||||
import itertools as it
|
||||
import logging
|
||||
from typing import Awaitable
|
||||
|
||||
import homeassistant.core as ha
|
||||
import homeassistant.config as conf_util
|
||||
@@ -109,7 +110,7 @@ def async_reload_core_config(hass):
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]:
|
||||
"""Set up general services related to Home Assistant."""
|
||||
@asyncio.coroutine
|
||||
def async_handle_turn_service(service):
|
||||
|
||||
@@ -26,20 +26,6 @@ ATTR_CHANGED_BY = 'changed_by'
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
SERVICE_TO_METHOD = {
|
||||
SERVICE_ALARM_DISARM: 'alarm_disarm',
|
||||
SERVICE_ALARM_ARM_HOME: 'alarm_arm_home',
|
||||
SERVICE_ALARM_ARM_AWAY: 'alarm_arm_away',
|
||||
SERVICE_ALARM_ARM_NIGHT: 'alarm_arm_night',
|
||||
SERVICE_ALARM_ARM_CUSTOM_BYPASS: 'alarm_arm_custom_bypass',
|
||||
SERVICE_ALARM_TRIGGER: 'alarm_trigger'
|
||||
}
|
||||
|
||||
ATTR_TO_PROPERTY = [
|
||||
ATTR_CODE,
|
||||
ATTR_CODE_FORMAT
|
||||
]
|
||||
|
||||
ALARM_SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Optional(ATTR_CODE): cv.string,
|
||||
@@ -126,36 +112,36 @@ def async_setup(hass, config):
|
||||
|
||||
yield from component.async_setup(config)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_alarm_service_handler(service):
|
||||
"""Map services to methods on Alarm."""
|
||||
target_alarms = component.async_extract_from_service(service)
|
||||
|
||||
code = service.data.get(ATTR_CODE)
|
||||
|
||||
method = "async_{}".format(SERVICE_TO_METHOD[service.service])
|
||||
|
||||
update_tasks = []
|
||||
for alarm in target_alarms:
|
||||
yield from getattr(alarm, method)(code)
|
||||
|
||||
if not alarm.should_poll:
|
||||
continue
|
||||
update_tasks.append(alarm.async_update_ha_state(True))
|
||||
|
||||
if update_tasks:
|
||||
yield from asyncio.wait(update_tasks, loop=hass.loop)
|
||||
|
||||
for service in SERVICE_TO_METHOD:
|
||||
hass.services.async_register(
|
||||
DOMAIN, service, async_alarm_service_handler,
|
||||
schema=ALARM_SERVICE_SCHEMA)
|
||||
component.async_register_entity_service(
|
||||
SERVICE_ALARM_DISARM, ALARM_SERVICE_SCHEMA,
|
||||
'async_alarm_disarm'
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
SERVICE_ALARM_ARM_HOME, ALARM_SERVICE_SCHEMA,
|
||||
'async_alarm_arm_home'
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
SERVICE_ALARM_ARM_AWAY, ALARM_SERVICE_SCHEMA,
|
||||
'async_alarm_arm_away'
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
SERVICE_ALARM_ARM_NIGHT, ALARM_SERVICE_SCHEMA,
|
||||
'async_alarm_arm_night'
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
SERVICE_ALARM_ARM_CUSTOM_BYPASS, ALARM_SERVICE_SCHEMA,
|
||||
'async_alarm_arm_custom_bypass'
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
SERVICE_ALARM_TRIGGER, ALARM_SERVICE_SCHEMA,
|
||||
'async_alarm_trigger'
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass, entry):
|
||||
"""Setup a config entry."""
|
||||
"""Set up a config entry."""
|
||||
return await hass.data[DOMAIN].async_setup_entry(entry)
|
||||
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
ICON = 'mdi:security'
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up an alarm control panel for an Abode device."""
|
||||
data = hass.data[ABODE_DOMAIN]
|
||||
|
||||
@@ -28,7 +28,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
data.devices.extend(alarm_devices)
|
||||
|
||||
add_devices(alarm_devices)
|
||||
add_entities(alarm_devices)
|
||||
|
||||
|
||||
class AbodeAlarm(AbodeDevice, AlarmControlPanel):
|
||||
|
||||
@@ -26,10 +26,10 @@ ALARM_TOGGLE_CHIME_SCHEMA = vol.Schema({
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up for AlarmDecoder alarm panels."""
|
||||
device = AlarmDecoderAlarmPanel()
|
||||
add_devices([device])
|
||||
add_entities([device])
|
||||
|
||||
def alarm_toggle_chime_handler(service):
|
||||
"""Register toggle chime handler."""
|
||||
|
||||
@@ -33,7 +33,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Set up a Alarm.com control panel."""
|
||||
name = config.get(CONF_NAME)
|
||||
code = config.get(CONF_CODE)
|
||||
@@ -42,7 +43,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
|
||||
alarmdotcom = AlarmDotCom(hass, name, code, username, password)
|
||||
yield from alarmdotcom.async_login()
|
||||
async_add_devices([alarmdotcom])
|
||||
async_add_entities([alarmdotcom])
|
||||
|
||||
|
||||
class AlarmDotCom(alarm.AlarmControlPanel):
|
||||
|
||||
@@ -38,7 +38,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the Arlo Alarm Control Panels."""
|
||||
arlo = hass.data[DATA_ARLO]
|
||||
|
||||
@@ -51,7 +51,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
for base_station in arlo.base_stations:
|
||||
base_stations.append(ArloBaseStation(base_station, home_mode_name,
|
||||
away_mode_name))
|
||||
add_devices(base_stations, True)
|
||||
add_entities(base_stations, True)
|
||||
|
||||
|
||||
class ArloBaseStation(AlarmControlPanel):
|
||||
|
||||
@@ -16,7 +16,7 @@ DEPENDENCIES = ['canary']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the Canary alarms."""
|
||||
data = hass.data[DATA_CANARY]
|
||||
devices = []
|
||||
@@ -24,7 +24,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
for location in data.locations:
|
||||
devices.append(CanaryAlarm(data, location.location_id))
|
||||
|
||||
add_devices(devices, True)
|
||||
add_entities(devices, True)
|
||||
|
||||
|
||||
class CanaryAlarm(AlarmControlPanel):
|
||||
|
||||
@@ -35,7 +35,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the Concord232 alarm control panel platform."""
|
||||
name = config.get(CONF_NAME)
|
||||
host = config.get(CONF_HOST)
|
||||
@@ -44,7 +44,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
url = 'http://{}:{}'.format(host, port)
|
||||
|
||||
try:
|
||||
add_devices([Concord232Alarm(hass, url, name)])
|
||||
add_entities([Concord232Alarm(hass, url, name)])
|
||||
except requests.exceptions.ConnectionError as ex:
|
||||
_LOGGER.error("Unable to connect to Concord232: %s", str(ex))
|
||||
return
|
||||
|
||||
@@ -13,9 +13,9 @@ from homeassistant.const import (
|
||||
CONF_PENDING_TIME, CONF_TRIGGER_TIME)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the Demo alarm control panel platform."""
|
||||
add_devices([
|
||||
add_entities([
|
||||
manual.ManualAlarm(hass, 'Alarm', '1234', None, False, {
|
||||
STATE_ALARM_ARMED_AWAY: {
|
||||
CONF_DELAY_TIME: datetime.timedelta(seconds=0),
|
||||
|
||||
@@ -34,7 +34,7 @@ STATES = {
|
||||
}
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the Egardia platform."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
@@ -45,7 +45,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
discovery_info.get(CONF_REPORT_SERVER_CODES),
|
||||
discovery_info[CONF_REPORT_SERVER_PORT])
|
||||
# add egardia alarm device
|
||||
add_devices([device], True)
|
||||
add_entities([device], True)
|
||||
|
||||
|
||||
class EgardiaAlarm(alarm.AlarmControlPanel):
|
||||
|
||||
@@ -33,7 +33,8 @@ ALARM_KEYPRESS_SCHEMA = vol.Schema({
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Perform the setup for Envisalink alarm panels."""
|
||||
configured_partitions = discovery_info['partitions']
|
||||
code = discovery_info[CONF_CODE]
|
||||
@@ -53,7 +54,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
)
|
||||
devices.append(device)
|
||||
|
||||
async_add_devices(devices)
|
||||
async_add_entities(devices)
|
||||
|
||||
@callback
|
||||
def alarm_keypress_handler(service):
|
||||
|
||||
@@ -1,36 +1,35 @@
|
||||
"""
|
||||
Support for HomematicIP alarm control panel.
|
||||
Support for HomematicIP Cloud alarm control panel.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.homematicip_cloud/
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.components.alarm_control_panel import AlarmControlPanel
|
||||
from homeassistant.components.homematicip_cloud import (
|
||||
HMIPC_HAPID, HomematicipGenericDevice)
|
||||
from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
|
||||
STATE_ALARM_TRIGGERED)
|
||||
from homeassistant.components.alarm_control_panel import AlarmControlPanel
|
||||
from homeassistant.components.homematicip_cloud import (
|
||||
HomematicipGenericDevice, DOMAIN as HMIPC_DOMAIN,
|
||||
HMIPC_HAPID)
|
||||
|
||||
|
||||
DEPENDENCIES = ['homematicip_cloud']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['homematicip_cloud']
|
||||
|
||||
HMIP_ZONE_AWAY = 'EXTERNAL'
|
||||
HMIP_ZONE_HOME = 'INTERNAL'
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
"""Set up the HomematicIP alarm control devices."""
|
||||
async def async_setup_platform(
|
||||
hass, config, async_add_entities, discovery_info=None):
|
||||
"""Set up the HomematicIP Cloud alarm control devices."""
|
||||
pass
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_devices):
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up the HomematicIP alarm control panel from a config entry."""
|
||||
from homematicip.aio.group import AsyncSecurityZoneGroup
|
||||
|
||||
@@ -41,11 +40,11 @@ async def async_setup_entry(hass, config_entry, async_add_devices):
|
||||
devices.append(HomematicipSecurityZone(home, group))
|
||||
|
||||
if devices:
|
||||
async_add_devices(devices)
|
||||
async_add_entities(devices)
|
||||
|
||||
|
||||
class HomematicipSecurityZone(HomematicipGenericDevice, AlarmControlPanel):
|
||||
"""Representation of an HomematicIP security zone group."""
|
||||
"""Representation of an HomematicIP Cloud security zone group."""
|
||||
|
||||
def __init__(self, home, device):
|
||||
"""Initialize the security zone group."""
|
||||
|
||||
@@ -40,7 +40,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up an iAlarm control panel."""
|
||||
name = config.get(CONF_NAME)
|
||||
username = config.get(CONF_USERNAME)
|
||||
@@ -49,7 +49,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
url = 'http://{}'.format(host)
|
||||
ialarm = IAlarmPanel(name, username, password, url)
|
||||
add_devices([ialarm], True)
|
||||
add_entities([ialarm], True)
|
||||
|
||||
|
||||
class IAlarmPanel(alarm.AlarmControlPanel):
|
||||
|
||||
@@ -59,7 +59,7 @@ PUSH_ALARM_STATE_SERVICE_SCHEMA = vol.Schema({
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up a control panel managed through IFTTT."""
|
||||
if DATA_IFTTT_ALARM not in hass.data:
|
||||
hass.data[DATA_IFTTT_ALARM] = []
|
||||
@@ -75,7 +75,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
alarmpanel = IFTTTAlarmPanel(name, code, event_away, event_home,
|
||||
event_night, event_disarm, optimistic)
|
||||
hass.data[DATA_IFTTT_ALARM].append(alarmpanel)
|
||||
add_devices([alarmpanel])
|
||||
add_entities([alarmpanel])
|
||||
|
||||
async def push_state_update(service):
|
||||
"""Set the service state as device state attribute."""
|
||||
|
||||
@@ -103,9 +103,9 @@ PLATFORM_SCHEMA = vol.Schema(vol.All({
|
||||
}, _state_validator))
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the manual alarm platform."""
|
||||
add_devices([ManualAlarm(
|
||||
add_entities([ManualAlarm(
|
||||
hass,
|
||||
config[CONF_NAME],
|
||||
config.get(CONF_CODE),
|
||||
|
||||
@@ -123,9 +123,9 @@ PLATFORM_SCHEMA = vol.Schema(vol.All(mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
|
||||
}), _state_validator))
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the manual MQTT alarm platform."""
|
||||
add_devices([ManualMQTTAlarm(
|
||||
add_entities([ManualMQTTAlarm(
|
||||
hass,
|
||||
config[CONF_NAME],
|
||||
config.get(CONF_CODE),
|
||||
|
||||
@@ -47,12 +47,13 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Set up the MQTT Alarm Control Panel platform."""
|
||||
if discovery_info is not None:
|
||||
config = PLATFORM_SCHEMA(discovery_info)
|
||||
|
||||
async_add_devices([MqttAlarm(
|
||||
async_add_entities([MqttAlarm(
|
||||
config.get(CONF_NAME),
|
||||
config.get(CONF_STATE_TOPIC),
|
||||
config.get(CONF_COMMAND_TOPIC),
|
||||
|
||||
@@ -31,7 +31,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the NX584 platform."""
|
||||
name = config.get(CONF_NAME)
|
||||
host = config.get(CONF_HOST)
|
||||
@@ -40,7 +40,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
url = 'http://{}:{}'.format(host, port)
|
||||
|
||||
try:
|
||||
add_devices([NX584Alarm(hass, url, name)])
|
||||
add_entities([NX584Alarm(hass, url, name)])
|
||||
except requests.exceptions.ConnectionError as ex:
|
||||
_LOGGER.error("Unable to connect to NX584: %s", str(ex))
|
||||
return False
|
||||
|
||||
@@ -19,14 +19,15 @@ DEPENDENCIES = ['satel_integra']
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Set up for Satel Integra alarm panels."""
|
||||
if not discovery_info:
|
||||
return
|
||||
|
||||
device = SatelIntegraAlarmPanel(
|
||||
"Alarm Panel", discovery_info.get(CONF_ARM_HOME_MODE))
|
||||
async_add_devices([device])
|
||||
async_add_entities([device])
|
||||
|
||||
|
||||
class SatelIntegraAlarmPanel(alarm.AlarmControlPanel):
|
||||
|
||||
@@ -34,7 +34,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the SimpliSafe platform."""
|
||||
from simplipy.api import SimpliSafeApiInterface, SimpliSafeAPIException
|
||||
name = config.get(CONF_NAME)
|
||||
@@ -45,7 +45,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
try:
|
||||
simplisafe = SimpliSafeApiInterface(username, password)
|
||||
except SimpliSafeAPIException:
|
||||
_LOGGER.error("Failed to setup SimpliSafe")
|
||||
_LOGGER.error("Failed to set up SimpliSafe")
|
||||
return
|
||||
|
||||
systems = []
|
||||
@@ -53,7 +53,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
for system in simplisafe.get_systems():
|
||||
systems.append(SimpliSafeAlarm(system, name, code))
|
||||
|
||||
add_devices(systems)
|
||||
add_entities(systems)
|
||||
|
||||
|
||||
class SimpliSafeAlarm(AlarmControlPanel):
|
||||
|
||||
@@ -29,7 +29,8 @@ def _get_alarm_state(spc_mode):
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Set up the SPC alarm control panel platform."""
|
||||
if (discovery_info is None or
|
||||
discovery_info[ATTR_DISCOVER_AREAS] is None):
|
||||
@@ -39,7 +40,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
devices = [SpcAlarm(api, area)
|
||||
for area in discovery_info[ATTR_DISCOVER_AREAS]]
|
||||
|
||||
async_add_devices(devices)
|
||||
async_add_entities(devices)
|
||||
|
||||
|
||||
class SpcAlarm(alarm.AlarmControlPanel):
|
||||
|
||||
@@ -31,14 +31,14 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up a TotalConnect control panel."""
|
||||
name = config.get(CONF_NAME)
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
|
||||
total_connect = TotalConnect(name, username, password)
|
||||
add_devices([total_connect], True)
|
||||
add_entities([total_connect], True)
|
||||
|
||||
|
||||
class TotalConnect(alarm.AlarmControlPanel):
|
||||
|
||||
@@ -17,13 +17,13 @@ from homeassistant.const import (
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the Verisure platform."""
|
||||
alarms = []
|
||||
if int(hub.config.get(CONF_ALARM, 1)):
|
||||
hub.update_overview()
|
||||
alarms.append(VerisureAlarm())
|
||||
add_devices(alarms)
|
||||
add_entities(alarms)
|
||||
|
||||
|
||||
def set_arm_state(state, code=None):
|
||||
|
||||
@@ -20,7 +20,7 @@ DEPENDENCIES = ['wink']
|
||||
STATE_ALARM_PRIVACY = 'Private'
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the Wink platform."""
|
||||
import pywink
|
||||
|
||||
@@ -32,7 +32,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
except AttributeError:
|
||||
_id = camera.object_id() + camera.name()
|
||||
if _id not in hass.data[DOMAIN]['unique_ids']:
|
||||
add_devices([WinkCameraDevice(camera, hass)])
|
||||
add_entities([WinkCameraDevice(camera, hass)])
|
||||
|
||||
|
||||
class WinkCameraDevice(WinkDevice, alarm.AlarmControlPanel):
|
||||
|
||||
@@ -111,6 +111,7 @@ def async_setup(hass, config):
|
||||
|
||||
for alert_id in alert_ids:
|
||||
alert = all_alerts[alert_id]
|
||||
alert.async_set_context(service_call.context)
|
||||
if service_call.service == SERVICE_TURN_ON:
|
||||
yield from alert.async_turn_on()
|
||||
elif service_call.service == SERVICE_TOGGLE:
|
||||
|
||||
@@ -13,12 +13,13 @@ import homeassistant.util.color as color_util
|
||||
from homeassistant.util.temperature import convert as convert_temperature
|
||||
from homeassistant.util.decorator import Registry
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, CONF_NAME,
|
||||
SERVICE_LOCK, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE,
|
||||
SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP,
|
||||
ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE,
|
||||
ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, SERVICE_LOCK,
|
||||
SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY,
|
||||
SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP,
|
||||
SERVICE_SET_COVER_POSITION, SERVICE_TURN_OFF, SERVICE_TURN_ON,
|
||||
SERVICE_UNLOCK, SERVICE_VOLUME_SET, TEMP_FAHRENHEIT, TEMP_CELSIUS,
|
||||
CONF_UNIT_OF_MEASUREMENT, STATE_LOCKED, STATE_UNLOCKED, STATE_ON)
|
||||
STATE_LOCKED, STATE_UNLOCKED, STATE_ON)
|
||||
|
||||
from .const import CONF_FILTER, CONF_ENTITY_CONFIG
|
||||
|
||||
@@ -53,6 +54,7 @@ CONF_DISPLAY_CATEGORIES = 'display_categories'
|
||||
|
||||
HANDLERS = Registry()
|
||||
ENTITY_ADAPTERS = Registry()
|
||||
EVENT_ALEXA_SMART_HOME = 'alexa_smart_home'
|
||||
|
||||
|
||||
class _DisplayCategory:
|
||||
@@ -159,7 +161,8 @@ class _AlexaEntity:
|
||||
The API handlers should manipulate entities only through this interface.
|
||||
"""
|
||||
|
||||
def __init__(self, config, entity):
|
||||
def __init__(self, hass, config, entity):
|
||||
self.hass = hass
|
||||
self.config = config
|
||||
self.entity = entity
|
||||
self.entity_conf = config.entity_config.get(entity.entity_id, {})
|
||||
@@ -383,6 +386,10 @@ class _AlexaInputController(_AlexaInterface):
|
||||
|
||||
|
||||
class _AlexaTemperatureSensor(_AlexaInterface):
|
||||
def __init__(self, hass, entity):
|
||||
_AlexaInterface.__init__(self, entity)
|
||||
self.hass = hass
|
||||
|
||||
def name(self):
|
||||
return 'Alexa.TemperatureSensor'
|
||||
|
||||
@@ -396,9 +403,10 @@ class _AlexaTemperatureSensor(_AlexaInterface):
|
||||
if name != 'temperature':
|
||||
raise _UnsupportedProperty(name)
|
||||
|
||||
unit = self.entity.attributes[CONF_UNIT_OF_MEASUREMENT]
|
||||
unit = self.entity.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||
temp = self.entity.state
|
||||
if self.entity.domain == climate.DOMAIN:
|
||||
unit = self.hass.config.units.temperature_unit
|
||||
temp = self.entity.attributes.get(
|
||||
climate.ATTR_CURRENT_TEMPERATURE)
|
||||
return {
|
||||
@@ -408,6 +416,10 @@ class _AlexaTemperatureSensor(_AlexaInterface):
|
||||
|
||||
|
||||
class _AlexaThermostatController(_AlexaInterface):
|
||||
def __init__(self, hass, entity):
|
||||
_AlexaInterface.__init__(self, entity)
|
||||
self.hass = hass
|
||||
|
||||
def name(self):
|
||||
return 'Alexa.ThermostatController'
|
||||
|
||||
@@ -438,8 +450,7 @@ class _AlexaThermostatController(_AlexaInterface):
|
||||
raise _UnsupportedProperty(name)
|
||||
return mode
|
||||
|
||||
unit = self.entity.attributes[CONF_UNIT_OF_MEASUREMENT]
|
||||
temp = None
|
||||
unit = self.hass.config.units.temperature_unit
|
||||
if name == 'targetSetpoint':
|
||||
temp = self.entity.attributes.get(climate.ATTR_TEMPERATURE)
|
||||
elif name == 'lowerSetpoint':
|
||||
@@ -490,8 +501,8 @@ class _ClimateCapabilities(_AlexaEntity):
|
||||
return [_DisplayCategory.THERMOSTAT]
|
||||
|
||||
def interfaces(self):
|
||||
yield _AlexaThermostatController(self.entity)
|
||||
yield _AlexaTemperatureSensor(self.entity)
|
||||
yield _AlexaThermostatController(self.hass, self.entity)
|
||||
yield _AlexaTemperatureSensor(self.hass, self.entity)
|
||||
|
||||
|
||||
@ENTITY_ADAPTERS.register(cover.DOMAIN)
|
||||
@@ -608,11 +619,11 @@ class _SensorCapabilities(_AlexaEntity):
|
||||
|
||||
def interfaces(self):
|
||||
attrs = self.entity.attributes
|
||||
if attrs.get(CONF_UNIT_OF_MEASUREMENT) in (
|
||||
if attrs.get(ATTR_UNIT_OF_MEASUREMENT) in (
|
||||
TEMP_FAHRENHEIT,
|
||||
TEMP_CELSIUS,
|
||||
):
|
||||
yield _AlexaTemperatureSensor(self.entity)
|
||||
yield _AlexaTemperatureSensor(self.hass, self.entity)
|
||||
|
||||
|
||||
class _Cause:
|
||||
@@ -703,24 +714,47 @@ class SmartHomeView(http.HomeAssistantView):
|
||||
return b'' if response is None else self.json(response)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_handle_message(hass, config, message):
|
||||
async def async_handle_message(hass, config, request, context=None):
|
||||
"""Handle incoming API messages."""
|
||||
assert message[API_DIRECTIVE][API_HEADER]['payloadVersion'] == '3'
|
||||
assert request[API_DIRECTIVE][API_HEADER]['payloadVersion'] == '3'
|
||||
|
||||
if context is None:
|
||||
context = ha.Context()
|
||||
|
||||
# Read head data
|
||||
message = message[API_DIRECTIVE]
|
||||
namespace = message[API_HEADER]['namespace']
|
||||
name = message[API_HEADER]['name']
|
||||
request = request[API_DIRECTIVE]
|
||||
namespace = request[API_HEADER]['namespace']
|
||||
name = request[API_HEADER]['name']
|
||||
|
||||
# Do we support this API request?
|
||||
funct_ref = HANDLERS.get((namespace, name))
|
||||
if not funct_ref:
|
||||
if funct_ref:
|
||||
response = await funct_ref(hass, config, request, context)
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
"Unsupported API request %s/%s", namespace, name)
|
||||
return api_error(message)
|
||||
response = api_error(request)
|
||||
|
||||
return (yield from funct_ref(hass, config, message))
|
||||
request_info = {
|
||||
'namespace': namespace,
|
||||
'name': name,
|
||||
}
|
||||
|
||||
if API_ENDPOINT in request and 'endpointId' in request[API_ENDPOINT]:
|
||||
request_info['entity_id'] = \
|
||||
request[API_ENDPOINT]['endpointId'].replace('#', '.')
|
||||
|
||||
response_header = response[API_EVENT][API_HEADER]
|
||||
|
||||
hass.bus.async_fire(EVENT_ALEXA_SMART_HOME, {
|
||||
'request': request_info,
|
||||
'response': {
|
||||
'namespace': response_header['namespace'],
|
||||
'name': response_header['name'],
|
||||
}
|
||||
}, context=context)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
def api_message(request,
|
||||
@@ -784,8 +818,7 @@ def api_error(request,
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.Discovery', 'Discover'))
|
||||
@asyncio.coroutine
|
||||
def async_api_discovery(hass, config, request):
|
||||
async def async_api_discovery(hass, config, request, context):
|
||||
"""Create a API formatted discovery response.
|
||||
|
||||
Async friendly.
|
||||
@@ -800,7 +833,7 @@ def async_api_discovery(hass, config, request):
|
||||
|
||||
if entity.domain not in ENTITY_ADAPTERS:
|
||||
continue
|
||||
alexa_entity = ENTITY_ADAPTERS[entity.domain](config, entity)
|
||||
alexa_entity = ENTITY_ADAPTERS[entity.domain](hass, config, entity)
|
||||
|
||||
endpoint = {
|
||||
'displayCategories': alexa_entity.display_categories(),
|
||||
@@ -827,8 +860,7 @@ def async_api_discovery(hass, config, request):
|
||||
|
||||
def extract_entity(funct):
|
||||
"""Decorate for extract entity object from request."""
|
||||
@asyncio.coroutine
|
||||
def async_api_entity_wrapper(hass, config, request):
|
||||
async def async_api_entity_wrapper(hass, config, request, context):
|
||||
"""Process a turn on request."""
|
||||
entity_id = request[API_ENDPOINT]['endpointId'].replace('#', '.')
|
||||
|
||||
@@ -839,15 +871,14 @@ def extract_entity(funct):
|
||||
request[API_HEADER]['name'], entity_id)
|
||||
return api_error(request, error_type='NO_SUCH_ENDPOINT')
|
||||
|
||||
return (yield from funct(hass, config, request, entity))
|
||||
return await funct(hass, config, request, context, entity)
|
||||
|
||||
return async_api_entity_wrapper
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.PowerController', 'TurnOn'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_turn_on(hass, config, request, entity):
|
||||
async def async_api_turn_on(hass, config, request, context, entity):
|
||||
"""Process a turn on request."""
|
||||
domain = entity.domain
|
||||
if entity.domain == group.DOMAIN:
|
||||
@@ -857,17 +888,16 @@ def async_api_turn_on(hass, config, request, entity):
|
||||
if entity.domain == cover.DOMAIN:
|
||||
service = cover.SERVICE_OPEN_COVER
|
||||
|
||||
yield from hass.services.async_call(domain, service, {
|
||||
await hass.services.async_call(domain, service, {
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}, blocking=False)
|
||||
}, blocking=False, context=context)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.PowerController', 'TurnOff'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_turn_off(hass, config, request, entity):
|
||||
async def async_api_turn_off(hass, config, request, context, entity):
|
||||
"""Process a turn off request."""
|
||||
domain = entity.domain
|
||||
if entity.domain == group.DOMAIN:
|
||||
@@ -877,32 +907,30 @@ def async_api_turn_off(hass, config, request, entity):
|
||||
if entity.domain == cover.DOMAIN:
|
||||
service = cover.SERVICE_CLOSE_COVER
|
||||
|
||||
yield from hass.services.async_call(domain, service, {
|
||||
await hass.services.async_call(domain, service, {
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}, blocking=False)
|
||||
}, blocking=False, context=context)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.BrightnessController', 'SetBrightness'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_set_brightness(hass, config, request, entity):
|
||||
async def async_api_set_brightness(hass, config, request, context, entity):
|
||||
"""Process a set brightness request."""
|
||||
brightness = int(request[API_PAYLOAD]['brightness'])
|
||||
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
||||
await hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
light.ATTR_BRIGHTNESS_PCT: brightness,
|
||||
}, blocking=False)
|
||||
}, blocking=False, context=context)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.BrightnessController', 'AdjustBrightness'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_adjust_brightness(hass, config, request, entity):
|
||||
async def async_api_adjust_brightness(hass, config, request, context, entity):
|
||||
"""Process an adjust brightness request."""
|
||||
brightness_delta = int(request[API_PAYLOAD]['brightnessDelta'])
|
||||
|
||||
@@ -915,18 +943,17 @@ def async_api_adjust_brightness(hass, config, request, entity):
|
||||
|
||||
# set brightness
|
||||
brightness = max(0, brightness_delta + current)
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
||||
await hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
light.ATTR_BRIGHTNESS_PCT: brightness,
|
||||
}, blocking=False)
|
||||
}, blocking=False, context=context)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.ColorController', 'SetColor'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_set_color(hass, config, request, entity):
|
||||
async def async_api_set_color(hass, config, request, context, entity):
|
||||
"""Process a set color request."""
|
||||
rgb = color_util.color_hsb_to_RGB(
|
||||
float(request[API_PAYLOAD]['color']['hue']),
|
||||
@@ -934,25 +961,25 @@ def async_api_set_color(hass, config, request, entity):
|
||||
float(request[API_PAYLOAD]['color']['brightness'])
|
||||
)
|
||||
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
||||
await hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
light.ATTR_RGB_COLOR: rgb,
|
||||
}, blocking=False)
|
||||
}, blocking=False, context=context)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.ColorTemperatureController', 'SetColorTemperature'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_set_color_temperature(hass, config, request, entity):
|
||||
async def async_api_set_color_temperature(hass, config, request, context,
|
||||
entity):
|
||||
"""Process a set color temperature request."""
|
||||
kelvin = int(request[API_PAYLOAD]['colorTemperatureInKelvin'])
|
||||
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
||||
await hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
light.ATTR_KELVIN: kelvin,
|
||||
}, blocking=False)
|
||||
}, blocking=False, context=context)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
@@ -960,17 +987,17 @@ def async_api_set_color_temperature(hass, config, request, entity):
|
||||
@HANDLERS.register(
|
||||
('Alexa.ColorTemperatureController', 'DecreaseColorTemperature'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_decrease_color_temp(hass, config, request, entity):
|
||||
async def async_api_decrease_color_temp(hass, config, request, context,
|
||||
entity):
|
||||
"""Process a decrease color temperature request."""
|
||||
current = int(entity.attributes.get(light.ATTR_COLOR_TEMP))
|
||||
max_mireds = int(entity.attributes.get(light.ATTR_MAX_MIREDS))
|
||||
|
||||
value = min(max_mireds, current + 50)
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
||||
await hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
light.ATTR_COLOR_TEMP: value,
|
||||
}, blocking=False)
|
||||
}, blocking=False, context=context)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
@@ -978,31 +1005,30 @@ def async_api_decrease_color_temp(hass, config, request, entity):
|
||||
@HANDLERS.register(
|
||||
('Alexa.ColorTemperatureController', 'IncreaseColorTemperature'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_increase_color_temp(hass, config, request, entity):
|
||||
async def async_api_increase_color_temp(hass, config, request, context,
|
||||
entity):
|
||||
"""Process an increase color temperature request."""
|
||||
current = int(entity.attributes.get(light.ATTR_COLOR_TEMP))
|
||||
min_mireds = int(entity.attributes.get(light.ATTR_MIN_MIREDS))
|
||||
|
||||
value = max(min_mireds, current - 50)
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
||||
await hass.services.async_call(entity.domain, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
light.ATTR_COLOR_TEMP: value,
|
||||
}, blocking=False)
|
||||
}, blocking=False, context=context)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.SceneController', 'Activate'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_activate(hass, config, request, entity):
|
||||
async def async_api_activate(hass, config, request, context, entity):
|
||||
"""Process an activate request."""
|
||||
domain = entity.domain
|
||||
|
||||
yield from hass.services.async_call(domain, SERVICE_TURN_ON, {
|
||||
await hass.services.async_call(domain, SERVICE_TURN_ON, {
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}, blocking=False)
|
||||
}, blocking=False, context=context)
|
||||
|
||||
payload = {
|
||||
'cause': {'type': _Cause.VOICE_INTERACTION},
|
||||
@@ -1019,14 +1045,13 @@ def async_api_activate(hass, config, request, entity):
|
||||
|
||||
@HANDLERS.register(('Alexa.SceneController', 'Deactivate'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_deactivate(hass, config, request, entity):
|
||||
async def async_api_deactivate(hass, config, request, context, entity):
|
||||
"""Process a deactivate request."""
|
||||
domain = entity.domain
|
||||
|
||||
yield from hass.services.async_call(domain, SERVICE_TURN_OFF, {
|
||||
await hass.services.async_call(domain, SERVICE_TURN_OFF, {
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}, blocking=False)
|
||||
}, blocking=False, context=context)
|
||||
|
||||
payload = {
|
||||
'cause': {'type': _Cause.VOICE_INTERACTION},
|
||||
@@ -1043,8 +1068,7 @@ def async_api_deactivate(hass, config, request, entity):
|
||||
|
||||
@HANDLERS.register(('Alexa.PercentageController', 'SetPercentage'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_set_percentage(hass, config, request, entity):
|
||||
async def async_api_set_percentage(hass, config, request, context, entity):
|
||||
"""Process a set percentage request."""
|
||||
percentage = int(request[API_PAYLOAD]['percentage'])
|
||||
service = None
|
||||
@@ -1066,16 +1090,15 @@ def async_api_set_percentage(hass, config, request, entity):
|
||||
service = SERVICE_SET_COVER_POSITION
|
||||
data[cover.ATTR_POSITION] = percentage
|
||||
|
||||
yield from hass.services.async_call(
|
||||
entity.domain, service, data, blocking=False)
|
||||
await hass.services.async_call(
|
||||
entity.domain, service, data, blocking=False, context=context)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.PercentageController', 'AdjustPercentage'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_adjust_percentage(hass, config, request, entity):
|
||||
async def async_api_adjust_percentage(hass, config, request, context, entity):
|
||||
"""Process an adjust percentage request."""
|
||||
percentage_delta = int(request[API_PAYLOAD]['percentageDelta'])
|
||||
service = None
|
||||
@@ -1114,20 +1137,19 @@ def async_api_adjust_percentage(hass, config, request, entity):
|
||||
|
||||
data[cover.ATTR_POSITION] = max(0, percentage_delta + current)
|
||||
|
||||
yield from hass.services.async_call(
|
||||
entity.domain, service, data, blocking=False)
|
||||
await hass.services.async_call(
|
||||
entity.domain, service, data, blocking=False, context=context)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.LockController', 'Lock'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_lock(hass, config, request, entity):
|
||||
async def async_api_lock(hass, config, request, context, entity):
|
||||
"""Process a lock request."""
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_LOCK, {
|
||||
await hass.services.async_call(entity.domain, SERVICE_LOCK, {
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}, blocking=False)
|
||||
}, blocking=False, context=context)
|
||||
|
||||
# Alexa expects a lockState in the response, we don't know the actual
|
||||
# lockState at this point but assume it is locked. It is reported
|
||||
@@ -1144,20 +1166,18 @@ def async_api_lock(hass, config, request, entity):
|
||||
# Not supported by Alexa yet
|
||||
@HANDLERS.register(('Alexa.LockController', 'Unlock'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_unlock(hass, config, request, entity):
|
||||
async def async_api_unlock(hass, config, request, context, entity):
|
||||
"""Process an unlock request."""
|
||||
yield from hass.services.async_call(entity.domain, SERVICE_UNLOCK, {
|
||||
await hass.services.async_call(entity.domain, SERVICE_UNLOCK, {
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}, blocking=False)
|
||||
}, blocking=False, context=context)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.Speaker', 'SetVolume'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_set_volume(hass, config, request, entity):
|
||||
async def async_api_set_volume(hass, config, request, context, entity):
|
||||
"""Process a set volume request."""
|
||||
volume = round(float(request[API_PAYLOAD]['volume'] / 100), 2)
|
||||
|
||||
@@ -1166,17 +1186,16 @@ def async_api_set_volume(hass, config, request, entity):
|
||||
media_player.ATTR_MEDIA_VOLUME_LEVEL: volume,
|
||||
}
|
||||
|
||||
yield from hass.services.async_call(
|
||||
await hass.services.async_call(
|
||||
entity.domain, SERVICE_VOLUME_SET,
|
||||
data, blocking=False)
|
||||
data, blocking=False, context=context)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.InputController', 'SelectInput'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_select_input(hass, config, request, entity):
|
||||
async def async_api_select_input(hass, config, request, context, entity):
|
||||
"""Process a set input request."""
|
||||
media_input = request[API_PAYLOAD]['input']
|
||||
|
||||
@@ -1200,17 +1219,16 @@ def async_api_select_input(hass, config, request, entity):
|
||||
media_player.ATTR_INPUT_SOURCE: media_input,
|
||||
}
|
||||
|
||||
yield from hass.services.async_call(
|
||||
await hass.services.async_call(
|
||||
entity.domain, media_player.SERVICE_SELECT_SOURCE,
|
||||
data, blocking=False)
|
||||
data, blocking=False, context=context)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.Speaker', 'AdjustVolume'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_adjust_volume(hass, config, request, entity):
|
||||
async def async_api_adjust_volume(hass, config, request, context, entity):
|
||||
"""Process an adjust volume request."""
|
||||
volume_delta = int(request[API_PAYLOAD]['volume'])
|
||||
|
||||
@@ -1229,17 +1247,16 @@ def async_api_adjust_volume(hass, config, request, entity):
|
||||
media_player.ATTR_MEDIA_VOLUME_LEVEL: volume,
|
||||
}
|
||||
|
||||
yield from hass.services.async_call(
|
||||
await hass.services.async_call(
|
||||
entity.domain, media_player.SERVICE_VOLUME_SET,
|
||||
data, blocking=False)
|
||||
data, blocking=False, context=context)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.StepSpeaker', 'AdjustVolume'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_adjust_volume_step(hass, config, request, entity):
|
||||
async def async_api_adjust_volume_step(hass, config, request, context, entity):
|
||||
"""Process an adjust volume step request."""
|
||||
# media_player volume up/down service does not support specifying steps
|
||||
# each component handles it differently e.g. via config.
|
||||
@@ -1252,13 +1269,13 @@ def async_api_adjust_volume_step(hass, config, request, entity):
|
||||
}
|
||||
|
||||
if volume_step > 0:
|
||||
yield from hass.services.async_call(
|
||||
await hass.services.async_call(
|
||||
entity.domain, media_player.SERVICE_VOLUME_UP,
|
||||
data, blocking=False)
|
||||
data, blocking=False, context=context)
|
||||
elif volume_step < 0:
|
||||
yield from hass.services.async_call(
|
||||
await hass.services.async_call(
|
||||
entity.domain, media_player.SERVICE_VOLUME_DOWN,
|
||||
data, blocking=False)
|
||||
data, blocking=False, context=context)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
@@ -1266,8 +1283,7 @@ def async_api_adjust_volume_step(hass, config, request, entity):
|
||||
@HANDLERS.register(('Alexa.StepSpeaker', 'SetMute'))
|
||||
@HANDLERS.register(('Alexa.Speaker', 'SetMute'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_set_mute(hass, config, request, entity):
|
||||
async def async_api_set_mute(hass, config, request, context, entity):
|
||||
"""Process a set mute request."""
|
||||
mute = bool(request[API_PAYLOAD]['mute'])
|
||||
|
||||
@@ -1276,98 +1292,94 @@ def async_api_set_mute(hass, config, request, entity):
|
||||
media_player.ATTR_MEDIA_VOLUME_MUTED: mute,
|
||||
}
|
||||
|
||||
yield from hass.services.async_call(
|
||||
await hass.services.async_call(
|
||||
entity.domain, media_player.SERVICE_VOLUME_MUTE,
|
||||
data, blocking=False)
|
||||
data, blocking=False, context=context)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.PlaybackController', 'Play'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_play(hass, config, request, entity):
|
||||
async def async_api_play(hass, config, request, context, entity):
|
||||
"""Process a play request."""
|
||||
data = {
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}
|
||||
|
||||
yield from hass.services.async_call(
|
||||
await hass.services.async_call(
|
||||
entity.domain, SERVICE_MEDIA_PLAY,
|
||||
data, blocking=False)
|
||||
data, blocking=False, context=context)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.PlaybackController', 'Pause'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_pause(hass, config, request, entity):
|
||||
async def async_api_pause(hass, config, request, context, entity):
|
||||
"""Process a pause request."""
|
||||
data = {
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}
|
||||
|
||||
yield from hass.services.async_call(
|
||||
await hass.services.async_call(
|
||||
entity.domain, SERVICE_MEDIA_PAUSE,
|
||||
data, blocking=False)
|
||||
data, blocking=False, context=context)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.PlaybackController', 'Stop'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_stop(hass, config, request, entity):
|
||||
async def async_api_stop(hass, config, request, context, entity):
|
||||
"""Process a stop request."""
|
||||
data = {
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}
|
||||
|
||||
yield from hass.services.async_call(
|
||||
await hass.services.async_call(
|
||||
entity.domain, SERVICE_MEDIA_STOP,
|
||||
data, blocking=False)
|
||||
data, blocking=False, context=context)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.PlaybackController', 'Next'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_next(hass, config, request, entity):
|
||||
async def async_api_next(hass, config, request, context, entity):
|
||||
"""Process a next request."""
|
||||
data = {
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}
|
||||
|
||||
yield from hass.services.async_call(
|
||||
await hass.services.async_call(
|
||||
entity.domain, SERVICE_MEDIA_NEXT_TRACK,
|
||||
data, blocking=False)
|
||||
data, blocking=False, context=context)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.PlaybackController', 'Previous'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_previous(hass, config, request, entity):
|
||||
async def async_api_previous(hass, config, request, context, entity):
|
||||
"""Process a previous request."""
|
||||
data = {
|
||||
ATTR_ENTITY_ID: entity.entity_id
|
||||
}
|
||||
|
||||
yield from hass.services.async_call(
|
||||
await hass.services.async_call(
|
||||
entity.domain, SERVICE_MEDIA_PREVIOUS_TRACK,
|
||||
data, blocking=False)
|
||||
data, blocking=False, context=context)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
|
||||
def api_error_temp_range(request, temp, min_temp, max_temp, unit):
|
||||
def api_error_temp_range(hass, request, temp, min_temp, max_temp):
|
||||
"""Create temperature value out of range API error response.
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
unit = hass.config.units.temperature_unit
|
||||
temp_range = {
|
||||
'minimumValue': {
|
||||
'value': min_temp,
|
||||
@@ -1388,8 +1400,9 @@ def api_error_temp_range(request, temp, min_temp, max_temp, unit):
|
||||
)
|
||||
|
||||
|
||||
def temperature_from_object(temp_obj, to_unit, interval=False):
|
||||
def temperature_from_object(hass, temp_obj, interval=False):
|
||||
"""Get temperature from Temperature object in requested unit."""
|
||||
to_unit = hass.config.units.temperature_unit
|
||||
from_unit = TEMP_CELSIUS
|
||||
temp = float(temp_obj['value'])
|
||||
|
||||
@@ -1405,9 +1418,8 @@ def temperature_from_object(temp_obj, to_unit, interval=False):
|
||||
|
||||
@HANDLERS.register(('Alexa.ThermostatController', 'SetTargetTemperature'))
|
||||
@extract_entity
|
||||
async def async_api_set_target_temp(hass, config, request, entity):
|
||||
async def async_api_set_target_temp(hass, config, request, context, entity):
|
||||
"""Process a set target temperature request."""
|
||||
unit = entity.attributes[CONF_UNIT_OF_MEASUREMENT]
|
||||
min_temp = entity.attributes.get(climate.ATTR_MIN_TEMP)
|
||||
max_temp = entity.attributes.get(climate.ATTR_MAX_TEMP)
|
||||
|
||||
@@ -1417,48 +1429,45 @@ async def async_api_set_target_temp(hass, config, request, entity):
|
||||
|
||||
payload = request[API_PAYLOAD]
|
||||
if 'targetSetpoint' in payload:
|
||||
temp = temperature_from_object(
|
||||
payload['targetSetpoint'], unit)
|
||||
temp = temperature_from_object(hass, payload['targetSetpoint'])
|
||||
if temp < min_temp or temp > max_temp:
|
||||
return api_error_temp_range(
|
||||
request, temp, min_temp, max_temp, unit)
|
||||
hass, request, temp, min_temp, max_temp)
|
||||
data[ATTR_TEMPERATURE] = temp
|
||||
if 'lowerSetpoint' in payload:
|
||||
temp_low = temperature_from_object(
|
||||
payload['lowerSetpoint'], unit)
|
||||
temp_low = temperature_from_object(hass, payload['lowerSetpoint'])
|
||||
if temp_low < min_temp or temp_low > max_temp:
|
||||
return api_error_temp_range(
|
||||
request, temp_low, min_temp, max_temp, unit)
|
||||
hass, request, temp_low, min_temp, max_temp)
|
||||
data[climate.ATTR_TARGET_TEMP_LOW] = temp_low
|
||||
if 'upperSetpoint' in payload:
|
||||
temp_high = temperature_from_object(
|
||||
payload['upperSetpoint'], unit)
|
||||
temp_high = temperature_from_object(hass, payload['upperSetpoint'])
|
||||
if temp_high < min_temp or temp_high > max_temp:
|
||||
return api_error_temp_range(
|
||||
request, temp_high, min_temp, max_temp, unit)
|
||||
hass, request, temp_high, min_temp, max_temp)
|
||||
data[climate.ATTR_TARGET_TEMP_HIGH] = temp_high
|
||||
|
||||
await hass.services.async_call(
|
||||
entity.domain, climate.SERVICE_SET_TEMPERATURE, data, blocking=False)
|
||||
entity.domain, climate.SERVICE_SET_TEMPERATURE, data, blocking=False,
|
||||
context=context)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.ThermostatController', 'AdjustTargetTemperature'))
|
||||
@extract_entity
|
||||
async def async_api_adjust_target_temp(hass, config, request, entity):
|
||||
async def async_api_adjust_target_temp(hass, config, request, context, entity):
|
||||
"""Process an adjust target temperature request."""
|
||||
unit = entity.attributes[CONF_UNIT_OF_MEASUREMENT]
|
||||
min_temp = entity.attributes.get(climate.ATTR_MIN_TEMP)
|
||||
max_temp = entity.attributes.get(climate.ATTR_MAX_TEMP)
|
||||
|
||||
temp_delta = temperature_from_object(
|
||||
request[API_PAYLOAD]['targetSetpointDelta'], unit, interval=True)
|
||||
hass, request[API_PAYLOAD]['targetSetpointDelta'], interval=True)
|
||||
target_temp = float(entity.attributes.get(ATTR_TEMPERATURE)) + temp_delta
|
||||
|
||||
if target_temp < min_temp or target_temp > max_temp:
|
||||
return api_error_temp_range(
|
||||
request, target_temp, min_temp, max_temp, unit)
|
||||
hass, request, target_temp, min_temp, max_temp)
|
||||
|
||||
data = {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
@@ -1466,14 +1475,16 @@ async def async_api_adjust_target_temp(hass, config, request, entity):
|
||||
}
|
||||
|
||||
await hass.services.async_call(
|
||||
entity.domain, climate.SERVICE_SET_TEMPERATURE, data, blocking=False)
|
||||
entity.domain, climate.SERVICE_SET_TEMPERATURE, data, blocking=False,
|
||||
context=context)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa.ThermostatController', 'SetThermostatMode'))
|
||||
@extract_entity
|
||||
async def async_api_set_thermostat_mode(hass, config, request, entity):
|
||||
async def async_api_set_thermostat_mode(hass, config, request, context,
|
||||
entity):
|
||||
"""Process a set thermostat mode request."""
|
||||
mode = request[API_PAYLOAD]['thermostatMode']
|
||||
mode = mode if isinstance(mode, str) else mode['value']
|
||||
@@ -1499,17 +1510,16 @@ async def async_api_set_thermostat_mode(hass, config, request, entity):
|
||||
|
||||
await hass.services.async_call(
|
||||
entity.domain, climate.SERVICE_SET_OPERATION_MODE, data,
|
||||
blocking=False)
|
||||
blocking=False, context=context)
|
||||
|
||||
return api_message(request)
|
||||
|
||||
|
||||
@HANDLERS.register(('Alexa', 'ReportState'))
|
||||
@extract_entity
|
||||
@asyncio.coroutine
|
||||
def async_api_reportstate(hass, config, request, entity):
|
||||
async def async_api_reportstate(hass, config, request, context, entity):
|
||||
"""Process a ReportState request."""
|
||||
alexa_entity = ENTITY_ADAPTERS[entity.domain](config, entity)
|
||||
alexa_entity = ENTITY_ADAPTERS[entity.domain](hass, config, entity)
|
||||
properties = []
|
||||
for interface in alexa_entity.interfaces():
|
||||
properties.extend(interface.serialize_properties())
|
||||
|
||||
@@ -24,7 +24,7 @@ from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.helpers import template
|
||||
from homeassistant.helpers.service import async_get_all_descriptions
|
||||
from homeassistant.helpers.state import AsyncTrackStates
|
||||
import homeassistant.remote as rem
|
||||
from homeassistant.helpers.json import JSONEncoder
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -102,7 +102,7 @@ class APIEventStream(HomeAssistantView):
|
||||
if event.event_type == EVENT_HOMEASSISTANT_STOP:
|
||||
data = stop_obj
|
||||
else:
|
||||
data = json.dumps(event, cls=rem.JSONEncoder)
|
||||
data = json.dumps(event, cls=JSONEncoder)
|
||||
|
||||
await to_write.put(data)
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_CONFIGURING = {}
|
||||
|
||||
REQUIREMENTS = ['py-august==0.4.0']
|
||||
REQUIREMENTS = ['py-august==0.6.0']
|
||||
|
||||
DEFAULT_TIMEOUT = 10
|
||||
ACTIVITY_FETCH_LIMIT = 10
|
||||
|
||||
16
homeassistant/components/auth/.translations/en.json
Normal file
16
homeassistant/components/auth/.translations/en.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"mfa_setup": {
|
||||
"totp": {
|
||||
"error": {
|
||||
"invalid_code": "Invalid code, please try again. If you get this error consistently, please make sure the clock on Home Assistant system is accurate."
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "Scan the QR code with your authentication app, such as **Google Authenticator** or **Authy**. If you have problem to scan the QR code, using **`{code}`** to manual setup. \n\n{qr_code}\n\nEnter the six digi code appeared in your app below to verify the setup:",
|
||||
"title": "Scan this QR code with your app"
|
||||
}
|
||||
},
|
||||
"title": "TOTP"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,21 +44,36 @@ a limited expiration.
|
||||
"expires_in": 1800,
|
||||
"token_type": "Bearer"
|
||||
}
|
||||
|
||||
## Revoking a refresh token
|
||||
|
||||
It is also possible to revoke a refresh token and all access tokens that have
|
||||
ever been granted by that refresh token. Response code will ALWAYS be 200.
|
||||
|
||||
{
|
||||
"token": "IJKLMNOPQRST",
|
||||
"action": "revoke"
|
||||
}
|
||||
|
||||
"""
|
||||
import logging
|
||||
import uuid
|
||||
from datetime import timedelta
|
||||
|
||||
from aiohttp import web
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.auth.models import User, Credentials
|
||||
from homeassistant.components import websocket_api
|
||||
from homeassistant.components.http.ban import log_invalid_auth
|
||||
from homeassistant.components.http.data_validator import RequestDataValidator
|
||||
from homeassistant.components.http.view import HomeAssistantView
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.core import callback, HomeAssistant
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import indieauth
|
||||
from . import login_flow
|
||||
from . import mfa_setup_flow
|
||||
|
||||
DOMAIN = 'auth'
|
||||
DEPENDENCIES = ['http']
|
||||
@@ -68,37 +83,41 @@ SCHEMA_WS_CURRENT_USER = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
||||
vol.Required('type'): WS_TYPE_CURRENT_USER,
|
||||
})
|
||||
|
||||
RESULT_TYPE_CREDENTIALS = 'credentials'
|
||||
RESULT_TYPE_USER = 'user'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup(hass, config):
|
||||
"""Component to allow users to login."""
|
||||
store_credentials, retrieve_credentials = _create_cred_store()
|
||||
store_result, retrieve_result = _create_auth_code_store()
|
||||
|
||||
hass.http.register_view(GrantTokenView(retrieve_credentials))
|
||||
hass.http.register_view(LinkUserView(retrieve_credentials))
|
||||
hass.http.register_view(TokenView(retrieve_result))
|
||||
hass.http.register_view(LinkUserView(retrieve_result))
|
||||
|
||||
hass.components.websocket_api.async_register_command(
|
||||
WS_TYPE_CURRENT_USER, websocket_current_user,
|
||||
SCHEMA_WS_CURRENT_USER
|
||||
)
|
||||
|
||||
await login_flow.async_setup(hass, store_credentials)
|
||||
await login_flow.async_setup(hass, store_result)
|
||||
await mfa_setup_flow.async_setup(hass)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class GrantTokenView(HomeAssistantView):
|
||||
"""View to grant tokens."""
|
||||
class TokenView(HomeAssistantView):
|
||||
"""View to issue or revoke tokens."""
|
||||
|
||||
url = '/auth/token'
|
||||
name = 'api:auth:token'
|
||||
requires_auth = False
|
||||
cors_allowed = True
|
||||
|
||||
def __init__(self, retrieve_credentials):
|
||||
"""Initialize the grant token view."""
|
||||
self._retrieve_credentials = retrieve_credentials
|
||||
def __init__(self, retrieve_user):
|
||||
"""Initialize the token view."""
|
||||
self._retrieve_user = retrieve_user
|
||||
|
||||
@log_invalid_auth
|
||||
async def post(self, request):
|
||||
@@ -108,6 +127,13 @@ class GrantTokenView(HomeAssistantView):
|
||||
|
||||
grant_type = data.get('grant_type')
|
||||
|
||||
# IndieAuth 6.3.5
|
||||
# The revocation endpoint is the same as the token endpoint.
|
||||
# The revocation request includes an additional parameter,
|
||||
# action=revoke.
|
||||
if data.get('action') == 'revoke':
|
||||
return await self._async_handle_revoke_token(hass, data)
|
||||
|
||||
if grant_type == 'authorization_code':
|
||||
return await self._async_handle_auth_code(hass, data)
|
||||
|
||||
@@ -118,6 +144,25 @@ class GrantTokenView(HomeAssistantView):
|
||||
'error': 'unsupported_grant_type',
|
||||
}, status_code=400)
|
||||
|
||||
async def _async_handle_revoke_token(self, hass, data):
|
||||
"""Handle revoke token request."""
|
||||
# OAuth 2.0 Token Revocation [RFC7009]
|
||||
# 2.2 The authorization server responds with HTTP status code 200
|
||||
# if the token has been revoked successfully or if the client
|
||||
# submitted an invalid token.
|
||||
token = data.get('token')
|
||||
|
||||
if token is None:
|
||||
return web.Response(status=200)
|
||||
|
||||
refresh_token = await hass.auth.async_get_refresh_token_by_token(token)
|
||||
|
||||
if refresh_token is None:
|
||||
return web.Response(status=200)
|
||||
|
||||
await hass.auth.async_remove_refresh_token(refresh_token)
|
||||
return web.Response(status=200)
|
||||
|
||||
async def _async_handle_auth_code(self, hass, data):
|
||||
"""Handle authorization code request."""
|
||||
client_id = data.get('client_id')
|
||||
@@ -132,17 +177,19 @@ class GrantTokenView(HomeAssistantView):
|
||||
if code is None:
|
||||
return self.json({
|
||||
'error': 'invalid_request',
|
||||
'error_description': 'Invalid code',
|
||||
}, status_code=400)
|
||||
|
||||
credentials = self._retrieve_credentials(client_id, code)
|
||||
user = self._retrieve_user(client_id, RESULT_TYPE_USER, code)
|
||||
|
||||
if credentials is None:
|
||||
if user is None or not isinstance(user, User):
|
||||
return self.json({
|
||||
'error': 'invalid_request',
|
||||
'error_description': 'Invalid code',
|
||||
}, status_code=400)
|
||||
|
||||
user = await hass.auth.async_get_or_create_user(credentials)
|
||||
# refresh user
|
||||
user = await hass.auth.async_get_user(user.id)
|
||||
|
||||
if not user.is_active:
|
||||
return self.json({
|
||||
@@ -155,7 +202,7 @@ class GrantTokenView(HomeAssistantView):
|
||||
access_token = hass.auth.async_create_access_token(refresh_token)
|
||||
|
||||
return self.json({
|
||||
'access_token': access_token.token,
|
||||
'access_token': access_token,
|
||||
'token_type': 'Bearer',
|
||||
'refresh_token': refresh_token.token,
|
||||
'expires_in':
|
||||
@@ -178,7 +225,7 @@ class GrantTokenView(HomeAssistantView):
|
||||
'error': 'invalid_request',
|
||||
}, status_code=400)
|
||||
|
||||
refresh_token = await hass.auth.async_get_refresh_token(token)
|
||||
refresh_token = await hass.auth.async_get_refresh_token_by_token(token)
|
||||
|
||||
if refresh_token is None:
|
||||
return self.json({
|
||||
@@ -193,7 +240,7 @@ class GrantTokenView(HomeAssistantView):
|
||||
access_token = hass.auth.async_create_access_token(refresh_token)
|
||||
|
||||
return self.json({
|
||||
'access_token': access_token.token,
|
||||
'access_token': access_token,
|
||||
'token_type': 'Bearer',
|
||||
'expires_in':
|
||||
int(refresh_token.access_token_expiration.total_seconds()),
|
||||
@@ -220,7 +267,7 @@ class LinkUserView(HomeAssistantView):
|
||||
user = request['hass_user']
|
||||
|
||||
credentials = self._retrieve_credentials(
|
||||
data['client_id'], data['code'])
|
||||
data['client_id'], RESULT_TYPE_CREDENTIALS, data['code'])
|
||||
|
||||
if credentials is None:
|
||||
return self.json_message('Invalid code', status_code=400)
|
||||
@@ -230,54 +277,69 @@ class LinkUserView(HomeAssistantView):
|
||||
|
||||
|
||||
@callback
|
||||
def _create_cred_store():
|
||||
"""Create a credential store."""
|
||||
temp_credentials = {}
|
||||
def _create_auth_code_store():
|
||||
"""Create an in memory store."""
|
||||
temp_results = {}
|
||||
|
||||
@callback
|
||||
def store_credentials(client_id, credentials):
|
||||
"""Store credentials and return a code to retrieve it."""
|
||||
def store_result(client_id, result):
|
||||
"""Store flow result and return a code to retrieve it."""
|
||||
if isinstance(result, User):
|
||||
result_type = RESULT_TYPE_USER
|
||||
elif isinstance(result, Credentials):
|
||||
result_type = RESULT_TYPE_CREDENTIALS
|
||||
else:
|
||||
raise ValueError('result has to be either User or Credentials')
|
||||
|
||||
code = uuid.uuid4().hex
|
||||
temp_credentials[(client_id, code)] = (dt_util.utcnow(), credentials)
|
||||
temp_results[(client_id, result_type, code)] = \
|
||||
(dt_util.utcnow(), result_type, result)
|
||||
return code
|
||||
|
||||
@callback
|
||||
def retrieve_credentials(client_id, code):
|
||||
"""Retrieve credentials."""
|
||||
key = (client_id, code)
|
||||
def retrieve_result(client_id, result_type, code):
|
||||
"""Retrieve flow result."""
|
||||
key = (client_id, result_type, code)
|
||||
|
||||
if key not in temp_credentials:
|
||||
if key not in temp_results:
|
||||
return None
|
||||
|
||||
created, credentials = temp_credentials.pop(key)
|
||||
created, _, result = temp_results.pop(key)
|
||||
|
||||
# OAuth 4.2.1
|
||||
# The authorization code MUST expire shortly after it is issued to
|
||||
# mitigate the risk of leaks. A maximum authorization code lifetime of
|
||||
# 10 minutes is RECOMMENDED.
|
||||
if dt_util.utcnow() - created < timedelta(minutes=10):
|
||||
return credentials
|
||||
return result
|
||||
|
||||
return None
|
||||
|
||||
return store_credentials, retrieve_credentials
|
||||
return store_result, retrieve_result
|
||||
|
||||
|
||||
@websocket_api.ws_require_user()
|
||||
@callback
|
||||
def websocket_current_user(hass, connection, msg):
|
||||
def websocket_current_user(
|
||||
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg):
|
||||
"""Return the current user."""
|
||||
user = connection.request.get('hass_user')
|
||||
async def async_get_current_user(user):
|
||||
"""Get current user."""
|
||||
enabled_modules = await hass.auth.async_get_enabled_mfa(user)
|
||||
|
||||
if user is None:
|
||||
connection.to_write.put_nowait(websocket_api.error_message(
|
||||
msg['id'], 'no_user', 'Not authenticated as a user'))
|
||||
return
|
||||
connection.send_message_outside(
|
||||
websocket_api.result_message(msg['id'], {
|
||||
'id': user.id,
|
||||
'name': user.name,
|
||||
'is_owner': user.is_owner,
|
||||
'credentials': [{'auth_provider_type': c.auth_provider_type,
|
||||
'auth_provider_id': c.auth_provider_id}
|
||||
for c in user.credentials],
|
||||
'mfa_modules': [{
|
||||
'id': module.id,
|
||||
'name': module.name,
|
||||
'enabled': module.id in enabled_modules,
|
||||
} for module in hass.auth.auth_mfa_modules],
|
||||
}))
|
||||
|
||||
connection.to_write.put_nowait(websocket_api.result_message(msg['id'], {
|
||||
'id': user.id,
|
||||
'name': user.name,
|
||||
'is_owner': user.is_owner,
|
||||
'credentials': [{'auth_provider_type': c.auth_provider_type,
|
||||
'auth_provider_id': c.auth_provider_id}
|
||||
for c in user.credentials]
|
||||
}))
|
||||
hass.async_create_task(async_get_current_user(connection.user))
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
"""Helpers to resolve client ID/secret."""
|
||||
import asyncio
|
||||
from html.parser import HTMLParser
|
||||
from ipaddress import ip_address, ip_network
|
||||
from urllib.parse import urlparse
|
||||
from urllib.parse import urlparse, urljoin
|
||||
|
||||
import aiohttp
|
||||
from aiohttp.client_exceptions import ClientError
|
||||
|
||||
# IP addresses of loopback interfaces
|
||||
ALLOWED_IPS = (
|
||||
@@ -16,7 +21,7 @@ ALLOWED_NETWORKS = (
|
||||
)
|
||||
|
||||
|
||||
def verify_redirect_uri(client_id, redirect_uri):
|
||||
async def verify_redirect_uri(hass, client_id, redirect_uri):
|
||||
"""Verify that the client and redirect uri match."""
|
||||
try:
|
||||
client_id_parts = _parse_client_id(client_id)
|
||||
@@ -25,16 +30,74 @@ def verify_redirect_uri(client_id, redirect_uri):
|
||||
|
||||
redirect_parts = _parse_url(redirect_uri)
|
||||
|
||||
# IndieAuth 4.2.2 allows for redirect_uri to be on different domain
|
||||
# but needs to be specified in link tag when fetching `client_id`.
|
||||
# This is not implemented.
|
||||
|
||||
# Verify redirect url and client url have same scheme and domain.
|
||||
return (
|
||||
is_valid = (
|
||||
client_id_parts.scheme == redirect_parts.scheme and
|
||||
client_id_parts.netloc == redirect_parts.netloc
|
||||
)
|
||||
|
||||
if is_valid:
|
||||
return True
|
||||
|
||||
# IndieAuth 4.2.2 allows for redirect_uri to be on different domain
|
||||
# but needs to be specified in link tag when fetching `client_id`.
|
||||
redirect_uris = await fetch_redirect_uris(hass, client_id)
|
||||
return redirect_uri in redirect_uris
|
||||
|
||||
|
||||
class LinkTagParser(HTMLParser):
|
||||
"""Parser to find link tags."""
|
||||
|
||||
def __init__(self, rel):
|
||||
"""Initialize a link tag parser."""
|
||||
super().__init__()
|
||||
self.rel = rel
|
||||
self.found = []
|
||||
|
||||
def handle_starttag(self, tag, attrs):
|
||||
"""Handle finding a start tag."""
|
||||
if tag != 'link':
|
||||
return
|
||||
|
||||
attrs = dict(attrs)
|
||||
|
||||
if attrs.get('rel') == self.rel:
|
||||
self.found.append(attrs.get('href'))
|
||||
|
||||
|
||||
async def fetch_redirect_uris(hass, url):
|
||||
"""Find link tag with redirect_uri values.
|
||||
|
||||
IndieAuth 4.2.2
|
||||
|
||||
The client SHOULD publish one or more <link> tags or Link HTTP headers with
|
||||
a rel attribute of redirect_uri at the client_id URL.
|
||||
|
||||
We limit to the first 10kB of the page.
|
||||
|
||||
We do not implement extracting redirect uris from headers.
|
||||
"""
|
||||
parser = LinkTagParser('redirect_uri')
|
||||
chunks = 0
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url, timeout=5) as resp:
|
||||
async for data in resp.content.iter_chunked(1024):
|
||||
parser.feed(data.decode())
|
||||
chunks += 1
|
||||
|
||||
if chunks == 10:
|
||||
break
|
||||
|
||||
except (asyncio.TimeoutError, ClientError):
|
||||
pass
|
||||
|
||||
# Authorization endpoints verifying that a redirect_uri is allowed for use
|
||||
# by a client MUST look for an exact match of the given redirect_uri in the
|
||||
# request against the list of redirect_uris discovered after resolving any
|
||||
# relative URLs.
|
||||
return [urljoin(url, found) for found in parser.found]
|
||||
|
||||
|
||||
def verify_client_id(client_id):
|
||||
"""Verify that the client id is valid."""
|
||||
|
||||
@@ -22,10 +22,14 @@ Pass in parameter 'client_id' and 'redirect_url' validate by indieauth.
|
||||
Pass in parameter 'handler' to specify the auth provider to use. Auth providers
|
||||
are identified by type and id.
|
||||
|
||||
And optional parameter 'type' has to set as 'link_user' if login flow used for
|
||||
link credential to exist user. Default 'type' is 'authorize'.
|
||||
|
||||
{
|
||||
"client_id": "https://hassbian.local:8123/",
|
||||
"handler": ["local_provider", null],
|
||||
"redirect_url": "https://hassbian.local:8123/"
|
||||
"redirect_url": "https://hassbian.local:8123/",
|
||||
"type': "authorize"
|
||||
}
|
||||
|
||||
Return value will be a step in a data entry flow. See the docs for data entry
|
||||
@@ -49,12 +53,14 @@ flow for details.
|
||||
Progress the flow. Most flows will be 1 page, but could optionally add extra
|
||||
login challenges, like TFA. Once the flow has finished, the returned step will
|
||||
have type "create_entry" and "result" key will contain an authorization code.
|
||||
The authorization code associated with an authorized user by default, it will
|
||||
associate with an credential if "type" set to "link_user" in
|
||||
"/auth/login_flow"
|
||||
|
||||
{
|
||||
"flow_id": "8f7e42faab604bcab7ac43c44ca34d58",
|
||||
"handler": ["insecure_example", null],
|
||||
"result": "411ee2f916e648d691e937ae9344681e",
|
||||
"source": "user",
|
||||
"title": "Example",
|
||||
"type": "create_entry",
|
||||
"version": 1
|
||||
@@ -64,21 +70,20 @@ import aiohttp.web
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import data_entry_flow
|
||||
from homeassistant.components.http import KEY_REAL_IP
|
||||
from homeassistant.components.http.ban import process_wrong_login, \
|
||||
log_invalid_auth
|
||||
from homeassistant.components.http.data_validator import RequestDataValidator
|
||||
from homeassistant.components.http.view import HomeAssistantView
|
||||
from homeassistant.helpers.data_entry_flow import (
|
||||
FlowManagerIndexView, FlowManagerResourceView)
|
||||
from . import indieauth
|
||||
|
||||
|
||||
async def async_setup(hass, store_credentials):
|
||||
async def async_setup(hass, store_result):
|
||||
"""Component to allow users to login."""
|
||||
hass.http.register_view(AuthProvidersView)
|
||||
hass.http.register_view(LoginFlowIndexView(hass.auth.login_flow))
|
||||
hass.http.register_view(
|
||||
LoginFlowResourceView(hass.auth.login_flow, store_credentials))
|
||||
LoginFlowResourceView(hass.auth.login_flow, store_result))
|
||||
|
||||
|
||||
class AuthProvidersView(HomeAssistantView):
|
||||
@@ -97,13 +102,41 @@ class AuthProvidersView(HomeAssistantView):
|
||||
} for provider in request.app['hass'].auth.auth_providers])
|
||||
|
||||
|
||||
class LoginFlowIndexView(FlowManagerIndexView):
|
||||
def _prepare_result_json(result):
|
||||
"""Convert result to JSON."""
|
||||
if result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
|
||||
data = result.copy()
|
||||
data.pop('result')
|
||||
data.pop('data')
|
||||
return data
|
||||
|
||||
if result['type'] != data_entry_flow.RESULT_TYPE_FORM:
|
||||
return result
|
||||
|
||||
import voluptuous_serialize
|
||||
|
||||
data = result.copy()
|
||||
|
||||
schema = data['data_schema']
|
||||
if schema is None:
|
||||
data['data_schema'] = []
|
||||
else:
|
||||
data['data_schema'] = voluptuous_serialize.convert(schema)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class LoginFlowIndexView(HomeAssistantView):
|
||||
"""View to create a config flow."""
|
||||
|
||||
url = '/auth/login_flow'
|
||||
name = 'api:auth:login_flow'
|
||||
requires_auth = False
|
||||
|
||||
def __init__(self, flow_mgr):
|
||||
"""Initialize the flow manager index view."""
|
||||
self._flow_mgr = flow_mgr
|
||||
|
||||
async def get(self, request):
|
||||
"""Do not allow index of flows in progress."""
|
||||
return aiohttp.web.Response(status=405)
|
||||
@@ -112,31 +145,47 @@ class LoginFlowIndexView(FlowManagerIndexView):
|
||||
vol.Required('client_id'): str,
|
||||
vol.Required('handler'): vol.Any(str, list),
|
||||
vol.Required('redirect_uri'): str,
|
||||
vol.Optional('type', default='authorize'): str,
|
||||
}))
|
||||
@log_invalid_auth
|
||||
async def post(self, request, data):
|
||||
"""Create a new login flow."""
|
||||
if not indieauth.verify_redirect_uri(data['client_id'],
|
||||
data['redirect_uri']):
|
||||
if not await indieauth.verify_redirect_uri(
|
||||
request.app['hass'], data['client_id'], data['redirect_uri']):
|
||||
return self.json_message('invalid client id or redirect uri', 400)
|
||||
|
||||
# pylint: disable=no-value-for-parameter
|
||||
return await super().post(request)
|
||||
if isinstance(data['handler'], list):
|
||||
handler = tuple(data['handler'])
|
||||
else:
|
||||
handler = data['handler']
|
||||
|
||||
try:
|
||||
result = await self._flow_mgr.async_init(
|
||||
handler, context={
|
||||
'ip_address': request[KEY_REAL_IP],
|
||||
'credential_only': data.get('type') == 'link_user',
|
||||
})
|
||||
except data_entry_flow.UnknownHandler:
|
||||
return self.json_message('Invalid handler specified', 404)
|
||||
except data_entry_flow.UnknownStep:
|
||||
return self.json_message('Handler does not support init', 400)
|
||||
|
||||
return self.json(_prepare_result_json(result))
|
||||
|
||||
|
||||
class LoginFlowResourceView(FlowManagerResourceView):
|
||||
class LoginFlowResourceView(HomeAssistantView):
|
||||
"""View to interact with the flow manager."""
|
||||
|
||||
url = '/auth/login_flow/{flow_id}'
|
||||
name = 'api:auth:login_flow:resource'
|
||||
requires_auth = False
|
||||
|
||||
def __init__(self, flow_mgr, store_credentials):
|
||||
def __init__(self, flow_mgr, store_result):
|
||||
"""Initialize the login flow resource view."""
|
||||
super().__init__(flow_mgr)
|
||||
self._store_credentials = store_credentials
|
||||
self._flow_mgr = flow_mgr
|
||||
self._store_result = store_result
|
||||
|
||||
async def get(self, request, flow_id):
|
||||
async def get(self, request):
|
||||
"""Do not allow getting status of a flow in progress."""
|
||||
return self.json_message('Invalid flow specified', 404)
|
||||
|
||||
@@ -152,6 +201,13 @@ class LoginFlowResourceView(FlowManagerResourceView):
|
||||
return self.json_message('Invalid client id', 400)
|
||||
|
||||
try:
|
||||
# do not allow change ip during login flow
|
||||
for flow in self._flow_mgr.async_progress():
|
||||
if (flow['flow_id'] == flow_id and
|
||||
flow['context']['ip_address'] !=
|
||||
request.get(KEY_REAL_IP)):
|
||||
return self.json_message('IP address changed', 400)
|
||||
|
||||
result = await self._flow_mgr.async_configure(flow_id, data)
|
||||
except data_entry_flow.UnknownFlow:
|
||||
return self.json_message('Invalid flow specified', 404)
|
||||
@@ -164,9 +220,18 @@ class LoginFlowResourceView(FlowManagerResourceView):
|
||||
if result['errors'] is not None and \
|
||||
result['errors'].get('base') == 'invalid_auth':
|
||||
await process_wrong_login(request)
|
||||
return self.json(self._prepare_result_json(result))
|
||||
return self.json(_prepare_result_json(result))
|
||||
|
||||
result.pop('data')
|
||||
result['result'] = self._store_credentials(client_id, result['result'])
|
||||
result['result'] = self._store_result(client_id, result['result'])
|
||||
|
||||
return self.json(result)
|
||||
|
||||
async def delete(self, request, flow_id):
|
||||
"""Cancel a flow in progress."""
|
||||
try:
|
||||
self._flow_mgr.async_abort(flow_id)
|
||||
except data_entry_flow.UnknownFlow:
|
||||
return self.json_message('Invalid flow specified', 404)
|
||||
|
||||
return self.json_message('Flow aborted')
|
||||
|
||||
134
homeassistant/components/auth/mfa_setup_flow.py
Normal file
134
homeassistant/components/auth/mfa_setup_flow.py
Normal file
@@ -0,0 +1,134 @@
|
||||
"""Helpers to setup multi-factor auth module."""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import data_entry_flow
|
||||
from homeassistant.components import websocket_api
|
||||
from homeassistant.core import callback, HomeAssistant
|
||||
|
||||
WS_TYPE_SETUP_MFA = 'auth/setup_mfa'
|
||||
SCHEMA_WS_SETUP_MFA = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
||||
vol.Required('type'): WS_TYPE_SETUP_MFA,
|
||||
vol.Exclusive('mfa_module_id', 'module_or_flow_id'): str,
|
||||
vol.Exclusive('flow_id', 'module_or_flow_id'): str,
|
||||
vol.Optional('user_input'): object,
|
||||
})
|
||||
|
||||
WS_TYPE_DEPOSE_MFA = 'auth/depose_mfa'
|
||||
SCHEMA_WS_DEPOSE_MFA = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
||||
vol.Required('type'): WS_TYPE_DEPOSE_MFA,
|
||||
vol.Required('mfa_module_id'): str,
|
||||
})
|
||||
|
||||
DATA_SETUP_FLOW_MGR = 'auth_mfa_setup_flow_manager'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup(hass):
|
||||
"""Init mfa setup flow manager."""
|
||||
async def _async_create_setup_flow(handler, context, data):
|
||||
"""Create a setup flow. hanlder is a mfa module."""
|
||||
mfa_module = hass.auth.get_auth_mfa_module(handler)
|
||||
if mfa_module is None:
|
||||
raise ValueError('Mfa module {} is not found'.format(handler))
|
||||
|
||||
user_id = data.pop('user_id')
|
||||
return await mfa_module.async_setup_flow(user_id)
|
||||
|
||||
async def _async_finish_setup_flow(flow, flow_result):
|
||||
_LOGGER.debug('flow_result: %s', flow_result)
|
||||
return flow_result
|
||||
|
||||
hass.data[DATA_SETUP_FLOW_MGR] = data_entry_flow.FlowManager(
|
||||
hass, _async_create_setup_flow, _async_finish_setup_flow)
|
||||
|
||||
hass.components.websocket_api.async_register_command(
|
||||
WS_TYPE_SETUP_MFA, websocket_setup_mfa, SCHEMA_WS_SETUP_MFA)
|
||||
|
||||
hass.components.websocket_api.async_register_command(
|
||||
WS_TYPE_DEPOSE_MFA, websocket_depose_mfa, SCHEMA_WS_DEPOSE_MFA)
|
||||
|
||||
|
||||
@callback
|
||||
@websocket_api.ws_require_user(allow_system_user=False)
|
||||
def websocket_setup_mfa(
|
||||
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg):
|
||||
"""Return a setup flow for mfa auth module."""
|
||||
async def async_setup_flow(msg):
|
||||
"""Return a setup flow for mfa auth module."""
|
||||
flow_manager = hass.data[DATA_SETUP_FLOW_MGR]
|
||||
|
||||
flow_id = msg.get('flow_id')
|
||||
if flow_id is not None:
|
||||
result = await flow_manager.async_configure(
|
||||
flow_id, msg.get('user_input'))
|
||||
connection.send_message_outside(
|
||||
websocket_api.result_message(
|
||||
msg['id'], _prepare_result_json(result)))
|
||||
return
|
||||
|
||||
mfa_module_id = msg.get('mfa_module_id')
|
||||
mfa_module = hass.auth.get_auth_mfa_module(mfa_module_id)
|
||||
if mfa_module is None:
|
||||
connection.send_message_outside(websocket_api.error_message(
|
||||
msg['id'], 'no_module',
|
||||
'MFA module {} is not found'.format(mfa_module_id)))
|
||||
return
|
||||
|
||||
result = await flow_manager.async_init(
|
||||
mfa_module_id, data={'user_id': connection.user.id})
|
||||
|
||||
connection.send_message_outside(
|
||||
websocket_api.result_message(
|
||||
msg['id'], _prepare_result_json(result)))
|
||||
|
||||
hass.async_create_task(async_setup_flow(msg))
|
||||
|
||||
|
||||
@callback
|
||||
@websocket_api.ws_require_user(allow_system_user=False)
|
||||
def websocket_depose_mfa(
|
||||
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg):
|
||||
"""Remove user from mfa module."""
|
||||
async def async_depose(msg):
|
||||
"""Remove user from mfa auth module."""
|
||||
mfa_module_id = msg['mfa_module_id']
|
||||
try:
|
||||
await hass.auth.async_disable_user_mfa(
|
||||
connection.user, msg['mfa_module_id'])
|
||||
except ValueError as err:
|
||||
connection.send_message_outside(websocket_api.error_message(
|
||||
msg['id'], 'disable_failed',
|
||||
'Cannot disable MFA Module {}: {}'.format(
|
||||
mfa_module_id, err)))
|
||||
return
|
||||
|
||||
connection.send_message_outside(
|
||||
websocket_api.result_message(
|
||||
msg['id'], 'done'))
|
||||
|
||||
hass.async_create_task(async_depose(msg))
|
||||
|
||||
|
||||
def _prepare_result_json(result):
|
||||
"""Convert result to JSON."""
|
||||
if result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
|
||||
data = result.copy()
|
||||
return data
|
||||
|
||||
if result['type'] != data_entry_flow.RESULT_TYPE_FORM:
|
||||
return result
|
||||
|
||||
import voluptuous_serialize
|
||||
|
||||
data = result.copy()
|
||||
|
||||
schema = data['data_schema']
|
||||
if schema is None:
|
||||
data['data_schema'] = []
|
||||
else:
|
||||
data['data_schema'] = voluptuous_serialize.convert(schema)
|
||||
|
||||
return data
|
||||
16
homeassistant/components/auth/strings.json
Normal file
16
homeassistant/components/auth/strings.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"mfa_setup":{
|
||||
"totp": {
|
||||
"title": "TOTP",
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Set up two-factor authentication using TOTP",
|
||||
"description": "To activate two factor authentication using time-based one-time passwords, scan the QR code with your authentication app. If you don't have one, we recommend either [Google Authenticator](https://support.google.com/accounts/answer/1066447) or [Authy](https://authy.com/).\n\n{qr_code}\n\nAfter scanning the code, enter the six digit code from your app to verify the setup. If you have problems scanning the QR code, do a manual setup with code **`{code}`**."
|
||||
}
|
||||
},
|
||||
"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,5 @@
|
||||
"""
|
||||
Allow to setup simple automation rules via the config file.
|
||||
Allow to set up simple automation rules via the config file.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/automation/
|
||||
|
||||
@@ -58,7 +58,7 @@ async def async_setup(hass, config):
|
||||
|
||||
|
||||
async def async_setup_entry(hass, entry):
|
||||
"""Setup a config entry."""
|
||||
"""Set up a config entry."""
|
||||
return await hass.data[DOMAIN].async_setup_entry(entry)
|
||||
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ DEPENDENCIES = ['abode']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up a sensor for an Abode device."""
|
||||
import abodepy.helpers.constants as CONST
|
||||
import abodepy.helpers.timeline as TIMELINE
|
||||
@@ -44,7 +44,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
data.devices.extend(devices)
|
||||
|
||||
add_devices(devices)
|
||||
add_entities(devices)
|
||||
|
||||
|
||||
class AbodeBinarySensor(AbodeDevice, BinarySensorDevice):
|
||||
|
||||
@@ -27,7 +27,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the Binary Sensor platform for ADS."""
|
||||
ads_hub = hass.data.get(DATA_ADS)
|
||||
|
||||
@@ -36,7 +36,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
device_class = config.get(CONF_DEVICE_CLASS)
|
||||
|
||||
ads_sensor = AdsBinarySensor(ads_hub, name, ads_var, device_class)
|
||||
add_devices([ads_sensor])
|
||||
add_entities([ads_sensor])
|
||||
|
||||
|
||||
class AdsBinarySensor(BinarySensorDevice):
|
||||
|
||||
@@ -28,7 +28,7 @@ ATTR_RF_LOOP4 = 'rf_loop4'
|
||||
ATTR_RF_LOOP1 = 'rf_loop1'
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the AlarmDecoder binary sensor devices."""
|
||||
configured_zones = discovery_info[CONF_ZONES]
|
||||
|
||||
@@ -44,7 +44,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
zone_num, zone_name, zone_type, zone_rfid, relay_addr, relay_chan)
|
||||
devices.append(device)
|
||||
|
||||
add_devices(devices)
|
||||
add_entities(devices)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -14,7 +14,8 @@ DEPENDENCIES = ['android_ip_webcam']
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Set up the IP Webcam binary sensors."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
@@ -23,7 +24,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
name = discovery_info[CONF_NAME]
|
||||
ipcam = hass.data[DATA_IP_WEBCAM][host]
|
||||
|
||||
async_add_devices(
|
||||
async_add_entities(
|
||||
[IPWebcamBinarySensor(name, host, ipcam, 'motion_active')], True)
|
||||
|
||||
|
||||
|
||||
@@ -20,9 +20,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up an APCUPSd Online Status binary sensor."""
|
||||
add_devices([OnlineStatus(config, apcupsd.DATA)], True)
|
||||
add_entities([OnlineStatus(config, apcupsd.DATA)], True)
|
||||
|
||||
|
||||
class OnlineStatus(BinarySensorDevice):
|
||||
|
||||
@@ -29,7 +29,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the aREST binary sensor."""
|
||||
resource = config.get(CONF_RESOURCE)
|
||||
pin = config.get(CONF_PIN)
|
||||
@@ -47,7 +47,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
arest = ArestData(resource, pin)
|
||||
|
||||
add_devices([ArestBinarySensor(
|
||||
add_entities([ArestBinarySensor(
|
||||
arest, resource, config.get(CONF_NAME, response[CONF_NAME]),
|
||||
device_class, pin)], True)
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ SENSOR_TYPES = {
|
||||
}
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the August binary sensors."""
|
||||
data = hass.data[DATA_AUGUST]
|
||||
devices = []
|
||||
@@ -62,7 +62,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
for sensor_type in SENSOR_TYPES:
|
||||
devices.append(AugustBinarySensor(data, sensor_type, doorbell))
|
||||
|
||||
add_devices(devices, True)
|
||||
add_entities(devices, True)
|
||||
|
||||
|
||||
class AugustBinarySensor(BinarySensorDevice):
|
||||
|
||||
@@ -39,7 +39,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the aurora sensor."""
|
||||
if None in (hass.config.latitude, hass.config.longitude):
|
||||
_LOGGER.error("Lat. or long. not set in Home Assistant config")
|
||||
@@ -57,7 +57,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"Connection to aurora forecast service failed: %s", error)
|
||||
return False
|
||||
|
||||
add_devices([AuroraSensor(aurora_data, name)], True)
|
||||
add_entities([AuroraSensor(aurora_data, name)], True)
|
||||
|
||||
|
||||
class AuroraSensor(BinarySensorDevice):
|
||||
|
||||
@@ -18,9 +18,9 @@ DEPENDENCIES = ['axis']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the Axis binary devices."""
|
||||
add_devices([AxisBinarySensor(hass, discovery_info)], True)
|
||||
add_entities([AxisBinarySensor(hass, discovery_info)], True)
|
||||
|
||||
|
||||
class AxisBinarySensor(AxisDeviceEvent, BinarySensorDevice):
|
||||
|
||||
@@ -75,7 +75,8 @@ def update_probability(prior, prob_true, prob_false):
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Set up the Bayesian Binary sensor."""
|
||||
name = config.get(CONF_NAME)
|
||||
observations = config.get(CONF_OBSERVATIONS)
|
||||
@@ -83,7 +84,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
probability_threshold = config.get(CONF_PROBABILITY_THRESHOLD)
|
||||
device_class = config.get(CONF_DEVICE_CLASS)
|
||||
|
||||
async_add_devices([
|
||||
async_add_entities([
|
||||
BayesianBinarySensor(
|
||||
name, prior, observations, probability_threshold, device_class)
|
||||
], True)
|
||||
@@ -122,7 +123,6 @@ class BayesianBinarySensor(BinarySensorDevice):
|
||||
def async_added_to_hass(self):
|
||||
"""Call when entity about to be added."""
|
||||
@callback
|
||||
# pylint: disable=invalid-name
|
||||
def async_threshold_sensor_state_listener(entity, old_state,
|
||||
new_state):
|
||||
"""Handle sensor state changes."""
|
||||
|
||||
@@ -41,7 +41,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the Beaglebone Black GPIO devices."""
|
||||
pins = config.get(CONF_PINS)
|
||||
|
||||
@@ -49,7 +49,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
for pin, params in pins.items():
|
||||
binary_sensors.append(BBBGPIOBinarySensor(pin, params))
|
||||
add_devices(binary_sensors)
|
||||
add_entities(binary_sensors)
|
||||
|
||||
|
||||
class BBBGPIOBinarySensor(BinarySensorDevice):
|
||||
|
||||
@@ -10,7 +10,7 @@ from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
DEPENDENCIES = ['blink']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the blink binary sensors."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
@@ -20,7 +20,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
for name in data.cameras:
|
||||
devs.append(BlinkCameraMotionSensor(name, data))
|
||||
devs.append(BlinkSystemSensor(data))
|
||||
add_devices(devs, True)
|
||||
add_entities(devs, True)
|
||||
|
||||
|
||||
class BlinkCameraMotionSensor(BinarySensorDevice):
|
||||
|
||||
@@ -28,7 +28,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the available BloomSky weather binary sensors."""
|
||||
bloomsky = hass.components.bloomsky
|
||||
# Default needed in case of discovery
|
||||
@@ -36,7 +36,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
for device in bloomsky.BLOOMSKY.devices.values():
|
||||
for variable in sensors:
|
||||
add_devices(
|
||||
add_entities(
|
||||
[BloomSkySensor(bloomsky.BLOOMSKY, device, variable)], True)
|
||||
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ SENSOR_TYPES_ELEC = {
|
||||
SENSOR_TYPES_ELEC.update(SENSOR_TYPES)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the BMW sensors."""
|
||||
accounts = hass.data[BMW_DOMAIN]
|
||||
_LOGGER.debug('Found BMW accounts: %s',
|
||||
@@ -51,7 +51,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
device = BMWConnectedDriveSensor(account, vehicle, key,
|
||||
value[0], value[1])
|
||||
devices.append(device)
|
||||
add_devices(devices, True)
|
||||
add_entities(devices, True)
|
||||
|
||||
|
||||
class BMWConnectedDriveSensor(BinarySensorDevice):
|
||||
@@ -71,7 +71,10 @@ class BMWConnectedDriveSensor(BinarySensorDevice):
|
||||
|
||||
@property
|
||||
def should_poll(self) -> bool:
|
||||
"""Data update is triggered from BMWConnectedDriveEntity."""
|
||||
"""Return False.
|
||||
|
||||
Data update is triggered from BMWConnectedDriveEntity.
|
||||
"""
|
||||
return False
|
||||
|
||||
@property
|
||||
|
||||
@@ -40,7 +40,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the Command line Binary Sensor."""
|
||||
name = config.get(CONF_NAME)
|
||||
command = config.get(CONF_COMMAND)
|
||||
@@ -53,7 +53,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
value_template.hass = hass
|
||||
data = CommandSensorData(hass, command, command_timeout)
|
||||
|
||||
add_devices([CommandBinarySensor(
|
||||
add_entities([CommandBinarySensor(
|
||||
hass, data, name, device_class, payload_on, payload_off,
|
||||
value_template)], True)
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the Concord232 binary sensor platform."""
|
||||
from concord232 import client as concord232_client
|
||||
|
||||
@@ -79,7 +79,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
)
|
||||
)
|
||||
|
||||
add_devices(sensors, True)
|
||||
add_entities(sensors, True)
|
||||
|
||||
|
||||
def get_opening_type(zone):
|
||||
|
||||
@@ -7,21 +7,22 @@ https://home-assistant.io/components/binary_sensor.deconz/
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.deconz.const import (
|
||||
ATTR_DARK, ATTR_ON, CONF_ALLOW_CLIP_SENSOR, DOMAIN as DATA_DECONZ,
|
||||
DATA_DECONZ_ID, DATA_DECONZ_UNSUB)
|
||||
DATA_DECONZ_ID, DATA_DECONZ_UNSUB, DECONZ_DOMAIN)
|
||||
from homeassistant.const import ATTR_BATTERY_LEVEL
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
DEPENDENCIES = ['deconz']
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_devices,
|
||||
async def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Old way of setting up deCONZ binary sensors."""
|
||||
pass
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_devices):
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up the deCONZ binary sensor."""
|
||||
@callback
|
||||
def async_add_sensor(sensors):
|
||||
@@ -33,7 +34,7 @@ async def async_setup_entry(hass, config_entry, async_add_devices):
|
||||
if sensor.type in DECONZ_BINARY_SENSOR and \
|
||||
not (not allow_clip_sensor and sensor.type.startswith('CLIP')):
|
||||
entities.append(DeconzBinarySensor(sensor))
|
||||
async_add_devices(entities, True)
|
||||
async_add_entities(entities, True)
|
||||
|
||||
hass.data[DATA_DECONZ_UNSUB].append(
|
||||
async_dispatcher_connect(hass, 'deconz_new_sensor', async_add_sensor))
|
||||
@@ -113,3 +114,19 @@ class DeconzBinarySensor(BinarySensorDevice):
|
||||
if self._sensor.type in PRESENCE and self._sensor.dark is not None:
|
||||
attr[ATTR_DARK] = self._sensor.dark
|
||||
return attr
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return a device description for device registry."""
|
||||
if (self._sensor.uniqueid is None or
|
||||
self._sensor.uniqueid.count(':') != 7):
|
||||
return None
|
||||
serial = self._sensor.uniqueid.split('-', 1)[0]
|
||||
return {
|
||||
'connections': {(CONNECTION_ZIGBEE, serial)},
|
||||
'identifiers': {(DECONZ_DOMAIN, serial)},
|
||||
'manufacturer': self._sensor.manufacturer,
|
||||
'model': self._sensor.modelid,
|
||||
'name': self._sensor.name,
|
||||
'sw_version': self._sensor.swversion,
|
||||
}
|
||||
|
||||
@@ -7,9 +7,9 @@ https://home-assistant.io/components/demo/
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the Demo binary sensor platform."""
|
||||
add_devices([
|
||||
add_entities([
|
||||
DemoBinarySensor('Basement Floor Wet', False, 'moisture'),
|
||||
DemoBinarySensor('Movement Backyard', True, 'motion'),
|
||||
])
|
||||
|
||||
@@ -28,7 +28,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the Digital Ocean droplet sensor."""
|
||||
digital = hass.data.get(DATA_DIGITAL_OCEAN)
|
||||
if not digital:
|
||||
@@ -44,7 +44,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
return False
|
||||
dev.append(DigitalOceanBinarySensor(digital, droplet_id))
|
||||
|
||||
add_devices(dev, True)
|
||||
add_entities(dev, True)
|
||||
|
||||
|
||||
class DigitalOceanBinarySensor(BinarySensorDevice):
|
||||
|
||||
@@ -12,7 +12,7 @@ DEPENDENCIES = ['ecobee']
|
||||
ECOBEE_CONFIG_FILE = 'ecobee.conf'
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the Ecobee sensors."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
@@ -26,7 +26,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
||||
dev.append(EcobeeBinarySensor(sensor['name'], index))
|
||||
|
||||
add_devices(dev, True)
|
||||
add_entities(dev, True)
|
||||
|
||||
|
||||
class EcobeeBinarySensor(BinarySensorDevice):
|
||||
|
||||
@@ -19,7 +19,8 @@ EGARDIA_TYPE_TO_DEVICE_CLASS = {'IR Sensor': 'motion',
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Initialize the platform."""
|
||||
if (discovery_info is None or
|
||||
discovery_info[ATTR_DISCOVER_DEVICES] is None):
|
||||
@@ -27,7 +28,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
|
||||
disc_info = discovery_info[ATTR_DISCOVER_DEVICES]
|
||||
# multiple devices here!
|
||||
async_add_devices(
|
||||
async_add_entities(
|
||||
(
|
||||
EgardiaBinarySensor(
|
||||
sensor_id=disc_info[sensor]['id'],
|
||||
@@ -58,7 +59,7 @@ class EgardiaBinarySensor(BinarySensorDevice):
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""The name of the device."""
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
@@ -74,5 +75,5 @@ class EgardiaBinarySensor(BinarySensorDevice):
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""The device class."""
|
||||
"""Return the device class."""
|
||||
return self._device_class
|
||||
|
||||
@@ -15,7 +15,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
DEPENDENCIES = ['eight_sleep']
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_devices,
|
||||
async def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Set up the eight sleep binary sensor."""
|
||||
if discovery_info is None:
|
||||
@@ -30,7 +30,7 @@ async def async_setup_platform(hass, config, async_add_devices,
|
||||
for sensor in sensors:
|
||||
all_sensors.append(EightHeatSensor(name, eight, sensor))
|
||||
|
||||
async_add_devices(all_sensors, True)
|
||||
async_add_entities(all_sensors, True)
|
||||
|
||||
|
||||
class EightHeatSensor(EightSleepHeatEntity, BinarySensorDevice):
|
||||
|
||||
@@ -27,13 +27,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the Binary Sensor platform for EnOcean."""
|
||||
dev_id = config.get(CONF_ID)
|
||||
devname = config.get(CONF_NAME)
|
||||
device_class = config.get(CONF_DEVICE_CLASS)
|
||||
|
||||
add_devices([EnOceanBinarySensor(dev_id, devname, device_class)])
|
||||
add_entities([EnOceanBinarySensor(dev_id, devname, device_class)])
|
||||
|
||||
|
||||
class EnOceanBinarySensor(enocean.EnOceanDevice, BinarySensorDevice):
|
||||
|
||||
@@ -23,7 +23,8 @@ DEPENDENCIES = ['envisalink']
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Set up the Envisalink binary sensor devices."""
|
||||
configured_zones = discovery_info['zones']
|
||||
|
||||
@@ -40,7 +41,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
)
|
||||
devices.append(device)
|
||||
|
||||
async_add_devices(devices)
|
||||
async_add_entities(devices)
|
||||
|
||||
|
||||
class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice):
|
||||
|
||||
@@ -47,7 +47,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Set up the FFmpeg binary motion sensor."""
|
||||
manager = hass.data[DATA_FFMPEG]
|
||||
|
||||
@@ -55,7 +56,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
return
|
||||
|
||||
entity = FFmpegMotion(hass, manager, config)
|
||||
async_add_devices([entity])
|
||||
async_add_entities([entity])
|
||||
|
||||
|
||||
class FFmpegBinarySensor(FFmpegBase, BinarySensorDevice):
|
||||
|
||||
@@ -44,7 +44,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Set up the FFmpeg noise binary sensor."""
|
||||
manager = hass.data[DATA_FFMPEG]
|
||||
|
||||
@@ -52,7 +53,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
return
|
||||
|
||||
entity = FFmpegNoise(hass, manager, config)
|
||||
async_add_devices([entity])
|
||||
async_add_entities([entity])
|
||||
|
||||
|
||||
class FFmpegNoise(FFmpegBinarySensor):
|
||||
|
||||
@@ -23,7 +23,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the GC100 devices."""
|
||||
binary_sensors = []
|
||||
ports = config.get(CONF_PORTS)
|
||||
@@ -31,7 +31,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
for port_addr, port_name in port.items():
|
||||
binary_sensors.append(GC100BinarySensor(
|
||||
port_name, port_addr, hass.data[DATA_GC100]))
|
||||
add_devices(binary_sensors, True)
|
||||
add_entities(binary_sensors, True)
|
||||
|
||||
|
||||
class GC100BinarySensor(BinarySensorDevice):
|
||||
|
||||
@@ -90,7 +90,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
data = HikvisionData(hass, url, port, name, username, password)
|
||||
|
||||
if data.sensors is None:
|
||||
_LOGGER.error("Hikvision event stream has no data, unable to setup")
|
||||
_LOGGER.error("Hikvision event stream has no data, unable to set up")
|
||||
return False
|
||||
|
||||
entities = []
|
||||
|
||||
@@ -13,13 +13,13 @@ DEVICETYPE_DEVICE_CLASS = {'motionsensor': 'motion',
|
||||
'contactsensor': 'opening'}
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up Hive sensor devices."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
session = hass.data.get(DATA_HIVE)
|
||||
|
||||
add_devices([HiveBinarySensorEntity(session, discovery_info)])
|
||||
add_entities([HiveBinarySensorEntity(session, discovery_info)])
|
||||
|
||||
|
||||
class HiveBinarySensorEntity(BinarySensorDevice):
|
||||
|
||||
@@ -5,31 +5,32 @@ For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.homematic/
|
||||
"""
|
||||
import logging
|
||||
from homeassistant.const import STATE_UNKNOWN
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.homematic import HMDevice, ATTR_DISCOVER_DEVICES
|
||||
from homeassistant.components.homematic import ATTR_DISCOVER_DEVICES, HMDevice
|
||||
from homeassistant.const import STATE_UNKNOWN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['homematic']
|
||||
|
||||
SENSOR_TYPES_CLASS = {
|
||||
'Remote': None,
|
||||
'ShutterContact': 'opening',
|
||||
'MaxShutterContact': 'opening',
|
||||
'IPShutterContact': 'opening',
|
||||
'Smoke': 'smoke',
|
||||
'SmokeV2': 'smoke',
|
||||
'MaxShutterContact': 'opening',
|
||||
'Motion': 'motion',
|
||||
'MotionV2': 'motion',
|
||||
'RemoteMotion': None,
|
||||
'WeatherSensor': None,
|
||||
'TiltSensor': None,
|
||||
'PresenceIP': 'motion',
|
||||
'Remote': None,
|
||||
'RemoteMotion': None,
|
||||
'ShutterContact': 'opening',
|
||||
'Smoke': 'smoke',
|
||||
'SmokeV2': 'smoke',
|
||||
'TiltSensor': None,
|
||||
'WeatherSensor': None,
|
||||
}
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the HomeMatic binary sensor platform."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
@@ -39,7 +40,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
new_device = HMBinarySensor(conf)
|
||||
devices.append(new_device)
|
||||
|
||||
add_devices(devices)
|
||||
add_entities(devices)
|
||||
|
||||
|
||||
class HMBinarySensor(HMDevice, BinarySensorDevice):
|
||||
|
||||
@@ -1,55 +1,50 @@
|
||||
"""
|
||||
Support for HomematicIP binary sensor.
|
||||
Support for HomematicIP Cloud binary sensor.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.homematicip_cloud/
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.homematicip_cloud import (
|
||||
HomematicipGenericDevice, DOMAIN as HMIPC_DOMAIN,
|
||||
HMIPC_HAPID)
|
||||
HMIPC_HAPID, HomematicipGenericDevice)
|
||||
from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN
|
||||
|
||||
DEPENDENCIES = ['homematicip_cloud']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_WINDOW_STATE = 'window_state'
|
||||
ATTR_EVENT_DELAY = 'event_delay'
|
||||
ATTR_MOTION_DETECTED = 'motion_detected'
|
||||
ATTR_ILLUMINATION = 'illumination'
|
||||
STATE_SMOKE_OFF = 'IDLE_OFF'
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
"""Set up the binary sensor devices."""
|
||||
async def async_setup_platform(
|
||||
hass, config, async_add_entities, discovery_info=None):
|
||||
"""Set up the HomematicIP Cloud binary sensor devices."""
|
||||
pass
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_devices):
|
||||
"""Set up the HomematicIP binary sensor from a config entry."""
|
||||
from homematicip.device import (ShutterContact, MotionDetectorIndoor)
|
||||
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)
|
||||
|
||||
home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home
|
||||
devices = []
|
||||
for device in home.devices:
|
||||
if isinstance(device, ShutterContact):
|
||||
if isinstance(device, AsyncShutterContact):
|
||||
devices.append(HomematicipShutterContact(home, device))
|
||||
elif isinstance(device, MotionDetectorIndoor):
|
||||
elif isinstance(device, AsyncMotionDetectorIndoor):
|
||||
devices.append(HomematicipMotionDetector(home, device))
|
||||
elif isinstance(device, AsyncSmokeDetector):
|
||||
devices.append(HomematicipSmokeDetector(home, device))
|
||||
|
||||
if devices:
|
||||
async_add_devices(devices)
|
||||
async_add_entities(devices)
|
||||
|
||||
|
||||
class HomematicipShutterContact(HomematicipGenericDevice, BinarySensorDevice):
|
||||
"""HomematicIP shutter contact."""
|
||||
|
||||
def __init__(self, home, device):
|
||||
"""Initialize the shutter contact."""
|
||||
super().__init__(home, device)
|
||||
"""Representation of a HomematicIP Cloud shutter contact."""
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
@@ -69,11 +64,7 @@ class HomematicipShutterContact(HomematicipGenericDevice, BinarySensorDevice):
|
||||
|
||||
|
||||
class HomematicipMotionDetector(HomematicipGenericDevice, BinarySensorDevice):
|
||||
"""MomematicIP motion detector."""
|
||||
|
||||
def __init__(self, home, device):
|
||||
"""Initialize the shutter contact."""
|
||||
super().__init__(home, device)
|
||||
"""Representation of a HomematicIP Cloud motion detector."""
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
@@ -86,3 +77,17 @@ class HomematicipMotionDetector(HomematicipGenericDevice, BinarySensorDevice):
|
||||
if self._device.sabotage:
|
||||
return True
|
||||
return self._device.motionDetected
|
||||
|
||||
|
||||
class HomematicipSmokeDetector(HomematicipGenericDevice, BinarySensorDevice):
|
||||
"""Representation of a HomematicIP Cloud smoke detector."""
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor."""
|
||||
return 'smoke'
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if smoke is detected."""
|
||||
return self._device.smokeDetectorAlarmType != STATE_SMOKE_OFF
|
||||
|
||||
@@ -26,7 +26,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up a sensor for a Hydrawise device."""
|
||||
hydrawise = hass.data[DATA_HYDRAWISE].data
|
||||
|
||||
@@ -45,7 +45,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
hydrawise.controller_status.get('running', False)
|
||||
sensors.append(HydrawiseBinarySensor(zone_data, sensor_type))
|
||||
|
||||
add_devices(sensors, True)
|
||||
add_entities(sensors, True)
|
||||
|
||||
|
||||
class HydrawiseBinarySensor(HydrawiseEntity, BinarySensorDevice):
|
||||
|
||||
@@ -30,7 +30,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the IHC binary sensor platform."""
|
||||
ihc_controller = hass.data[IHC_DATA][IHC_CONTROLLER]
|
||||
info = hass.data[IHC_DATA][IHC_INFO]
|
||||
@@ -56,7 +56,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
sensor_type, inverting)
|
||||
devices.append(sensor)
|
||||
|
||||
add_devices(devices)
|
||||
add_entities(devices)
|
||||
|
||||
|
||||
class IHCBinarySensor(IHCDevice, BinarySensorDevice):
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
Support for INSTEON dimmers via PowerLinc Modem.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.insteon_plm/
|
||||
https://home-assistant.io/components/binary_sensor.insteon/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.insteon_plm import InsteonPLMEntity
|
||||
from homeassistant.components.insteon import InsteonEntity
|
||||
|
||||
DEPENDENCIES = ['insteon_plm']
|
||||
DEPENDENCIES = ['insteon']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -23,28 +23,29 @@ SENSOR_TYPES = {'openClosedSensor': 'opening',
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up the INSTEON PLM device class for the hass platform."""
|
||||
plm = hass.data['insteon_plm'].get('plm')
|
||||
def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Set up the INSTEON device class for the hass platform."""
|
||||
insteon_modem = hass.data['insteon'].get('modem')
|
||||
|
||||
address = discovery_info['address']
|
||||
device = plm.devices[address]
|
||||
device = insteon_modem.devices[address]
|
||||
state_key = discovery_info['state_key']
|
||||
name = device.states[state_key].name
|
||||
if name != 'dryLeakSensor':
|
||||
_LOGGER.debug('Adding device %s entity %s to Binary Sensor platform',
|
||||
device.address.hex, device.states[state_key].name)
|
||||
|
||||
new_entity = InsteonPLMBinarySensor(device, state_key)
|
||||
new_entity = InsteonBinarySensor(device, state_key)
|
||||
|
||||
async_add_devices([new_entity])
|
||||
async_add_entities([new_entity])
|
||||
|
||||
|
||||
class InsteonPLMBinarySensor(InsteonPLMEntity, BinarySensorDevice):
|
||||
class InsteonBinarySensor(InsteonEntity, BinarySensorDevice):
|
||||
"""A Class for an Insteon device entity."""
|
||||
|
||||
def __init__(self, device, state_key):
|
||||
"""Initialize the INSTEON PLM binary sensor."""
|
||||
"""Initialize the INSTEON binary sensor."""
|
||||
super().__init__(device, state_key)
|
||||
self._sensor_type = SENSOR_TYPES.get(self._insteon_device_state.name)
|
||||
|
||||
@@ -35,7 +35,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the ISS sensor."""
|
||||
if None in (hass.config.latitude, hass.config.longitude):
|
||||
_LOGGER.error("Latitude or longitude not set in Home Assistant config")
|
||||
@@ -51,7 +51,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
name = config.get(CONF_NAME)
|
||||
show_on_map = config.get(CONF_SHOW_ON_MAP)
|
||||
|
||||
add_devices([IssBinarySensor(iss_data, name, show_on_map)], True)
|
||||
add_entities([IssBinarySensor(iss_data, name, show_on_map)], True)
|
||||
|
||||
|
||||
class IssBinarySensor(BinarySensorDevice):
|
||||
|
||||
@@ -29,7 +29,7 @@ ISY_DEVICE_TYPES = {
|
||||
|
||||
|
||||
def setup_platform(hass, config: ConfigType,
|
||||
add_devices: Callable[[list], None], discovery_info=None):
|
||||
add_entities: Callable[[list], None], discovery_info=None):
|
||||
"""Set up the ISY994 binary sensor platform."""
|
||||
devices = []
|
||||
devices_by_nid = {}
|
||||
@@ -75,7 +75,7 @@ def setup_platform(hass, config: ConfigType,
|
||||
for name, status, _ in hass.data[ISY994_PROGRAMS][DOMAIN]:
|
||||
devices.append(ISYBinarySensorProgram(name, status))
|
||||
|
||||
add_devices(devices)
|
||||
add_entities(devices)
|
||||
|
||||
|
||||
def _detect_device_type(node) -> str:
|
||||
|
||||
@@ -54,27 +54,27 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_devices,
|
||||
async def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Set up binary sensor(s) for KNX platform."""
|
||||
if discovery_info is not None:
|
||||
async_add_devices_discovery(hass, discovery_info, async_add_devices)
|
||||
async_add_entities_discovery(hass, discovery_info, async_add_entities)
|
||||
else:
|
||||
async_add_devices_config(hass, config, async_add_devices)
|
||||
async_add_entities_config(hass, config, async_add_entities)
|
||||
|
||||
|
||||
@callback
|
||||
def async_add_devices_discovery(hass, discovery_info, async_add_devices):
|
||||
def async_add_entities_discovery(hass, discovery_info, async_add_entities):
|
||||
"""Set up binary sensors for KNX platform configured via xknx.yaml."""
|
||||
entities = []
|
||||
for device_name in discovery_info[ATTR_DISCOVER_DEVICES]:
|
||||
device = hass.data[DATA_KNX].xknx.devices[device_name]
|
||||
entities.append(KNXBinarySensor(hass, device))
|
||||
async_add_devices(entities)
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
@callback
|
||||
def async_add_devices_config(hass, config, async_add_devices):
|
||||
def async_add_entities_config(hass, config, async_add_entities):
|
||||
"""Set up binary senor for KNX platform configured within platform."""
|
||||
name = config.get(CONF_NAME)
|
||||
import xknx
|
||||
@@ -97,7 +97,7 @@ def async_add_devices_config(hass, config, async_add_devices):
|
||||
entity.automations.append(KNXAutomation(
|
||||
hass=hass, device=binary_sensor, hook=hook,
|
||||
action=action, counter=counter))
|
||||
async_add_devices([entity])
|
||||
async_add_entities([entity])
|
||||
|
||||
|
||||
class KNXBinarySensor(BinarySensorDevice):
|
||||
|
||||
@@ -20,7 +20,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
DEPENDENCIES = ['konnected']
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_devices,
|
||||
async def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Set up binary sensors attached to a Konnected device."""
|
||||
if discovery_info is None:
|
||||
@@ -31,7 +31,7 @@ async def async_setup_platform(hass, config, async_add_devices,
|
||||
sensors = [KonnectedBinarySensor(device_id, pin_num, pin_data)
|
||||
for pin_num, pin_data in
|
||||
data[CONF_DEVICES][device_id][CONF_BINARY_SENSORS].items()]
|
||||
async_add_devices(sensors)
|
||||
async_add_entities(sensors)
|
||||
|
||||
|
||||
class KonnectedBinarySensor(BinarySensorDevice):
|
||||
|
||||
@@ -27,7 +27,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the Linode droplet sensor."""
|
||||
linode = hass.data.get(DATA_LINODE)
|
||||
nodes = config.get(CONF_NODES)
|
||||
@@ -40,7 +40,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
return
|
||||
dev.append(LinodeBinarySensor(linode, node_id))
|
||||
|
||||
add_devices(dev, True)
|
||||
add_entities(dev, True)
|
||||
|
||||
|
||||
class LinodeBinarySensor(BinarySensorDevice):
|
||||
|
||||
@@ -13,7 +13,7 @@ from homeassistant.const import STATE_UNKNOWN
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Iterate through all MAX! Devices and add window shutters."""
|
||||
devices = []
|
||||
for handler in hass.data[DATA_KEY].values():
|
||||
@@ -28,7 +28,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
MaxCubeShutter(handler, name, device.rf_address))
|
||||
|
||||
if devices:
|
||||
add_devices(devices)
|
||||
add_entities(devices)
|
||||
|
||||
|
||||
class MaxCubeShutter(BinarySensorDevice):
|
||||
|
||||
@@ -28,7 +28,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the Modbus binary sensors."""
|
||||
sensors = []
|
||||
for coil in config.get(CONF_COILS):
|
||||
@@ -36,7 +36,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
coil.get(CONF_NAME),
|
||||
coil.get(CONF_SLAVE),
|
||||
coil.get(CONF_COIL)))
|
||||
add_devices(sensors)
|
||||
add_entities(sensors)
|
||||
|
||||
|
||||
class ModbusCoilSensor(BinarySensorDevice):
|
||||
|
||||
@@ -45,7 +45,8 @@ PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Set up the MQTT binary sensor."""
|
||||
if discovery_info is not None:
|
||||
config = PLATFORM_SCHEMA(discovery_info)
|
||||
@@ -54,7 +55,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
if value_template is not None:
|
||||
value_template.hass = hass
|
||||
|
||||
async_add_devices([MqttBinarySensor(
|
||||
async_add_entities([MqttBinarySensor(
|
||||
config.get(CONF_NAME),
|
||||
config.get(CONF_STATE_TOPIC),
|
||||
config.get(CONF_AVAILABILITY_TOPIC),
|
||||
|
||||
@@ -23,7 +23,8 @@ SENSORS = [
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Set up the MyChevy sensors."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
@@ -34,7 +35,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
for car in hub.cars:
|
||||
sensors.append(EVBinarySensor(hub, sconfig, car.vid))
|
||||
|
||||
async_add_devices(sensors)
|
||||
async_add_entities(sensors)
|
||||
|
||||
|
||||
class EVBinarySensor(BinarySensorDevice):
|
||||
|
||||
@@ -22,11 +22,11 @@ SENSORS = {
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
hass, config, async_add_devices, discovery_info=None):
|
||||
hass, config, async_add_entities, discovery_info=None):
|
||||
"""Set up the mysensors platform for binary sensors."""
|
||||
mysensors.setup_mysensors_platform(
|
||||
hass, DOMAIN, discovery_info, MySensorsBinarySensor,
|
||||
async_add_devices=async_add_devices)
|
||||
async_add_entities=async_add_entities)
|
||||
|
||||
|
||||
class MySensorsBinarySensor(
|
||||
|
||||
@@ -17,9 +17,10 @@ DEPENDENCIES = ['http']
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
def async_setup_platform(hass, config, async_add_entities,
|
||||
discovery_info=None):
|
||||
"""Set up myStrom Binary Sensor."""
|
||||
hass.http.register_view(MyStromView(async_add_devices))
|
||||
hass.http.register_view(MyStromView(async_add_entities))
|
||||
|
||||
return True
|
||||
|
||||
@@ -31,10 +32,10 @@ class MyStromView(HomeAssistantView):
|
||||
name = 'api:mystrom'
|
||||
supported_actions = ['single', 'double', 'long', 'touch']
|
||||
|
||||
def __init__(self, add_devices):
|
||||
def __init__(self, add_entities):
|
||||
"""Initialize the myStrom URL endpoint."""
|
||||
self.buttons = {}
|
||||
self.add_devices = add_devices
|
||||
self.add_entities = add_entities
|
||||
|
||||
@asyncio.coroutine
|
||||
def get(self, request):
|
||||
@@ -62,7 +63,7 @@ class MyStromView(HomeAssistantView):
|
||||
button_id, button_action)
|
||||
self.buttons[entity_id] = MyStromBinarySensor(
|
||||
'{}_{}'.format(button_id, button_action))
|
||||
hass.async_add_job(self.add_devices, [self.buttons[entity_id]])
|
||||
self.add_entities([self.buttons[entity_id]])
|
||||
else:
|
||||
new_state = True if self.buttons[entity_id].state == 'off' \
|
||||
else False
|
||||
|
||||
@@ -54,14 +54,14 @@ _VALID_BINARY_SENSOR_TYPES = {**BINARY_TYPES, **CLIMATE_BINARY_TYPES,
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the Nest binary sensors.
|
||||
|
||||
No longer used.
|
||||
"""
|
||||
|
||||
|
||||
async def async_setup_entry(hass, entry, async_add_devices):
|
||||
async def async_setup_entry(hass, entry, async_add_entities):
|
||||
"""Set up a Nest binary sensor based on a config entry."""
|
||||
nest = hass.data[DATA_NEST]
|
||||
|
||||
@@ -112,7 +112,7 @@ async def async_setup_entry(hass, entry, async_add_devices):
|
||||
|
||||
return sensors
|
||||
|
||||
async_add_devices(await hass.async_add_job(get_binary_sensors), True)
|
||||
async_add_entities(await hass.async_add_job(get_binary_sensors), True)
|
||||
|
||||
|
||||
class NestBinarySensor(NestSensorDevice, BinarySensorDevice):
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user