mirror of
https://github.com/home-assistant/core.git
synced 2026-01-04 23:05:26 +01:00
Compare commits
234 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
3f8c91d77c | ||
|
|
ef5095cf53 | ||
|
|
5015071816 | ||
|
|
b110a80fbd | ||
|
|
c7a8f1143c | ||
|
|
dbe44c076e | ||
|
|
3246b49a45 | ||
|
|
c482d48fde | ||
|
|
0c7d46927e | ||
|
|
7d9f8b0d4c | ||
|
|
b8981b2675 | ||
|
|
f6935b5d27 | ||
|
|
6028db21ab | ||
|
|
c63fd974fb | ||
|
|
cdb86ed154 | ||
|
|
0f844311c9 | ||
|
|
91e8680fc5 | ||
|
|
6f2000f5e2 | ||
|
|
8d2359026c | ||
|
|
ee180c51cf | ||
|
|
b63312ff2e | ||
|
|
59f8a73676 | ||
|
|
affd4e7df3 | ||
|
|
38928c4c0e | ||
|
|
48af5116b3 | ||
|
|
a5112f317d | ||
|
|
eb5f6efb43 | ||
|
|
3ed47b05a5 | ||
|
|
7972d6a0c6 | ||
|
|
163cd72b7a | ||
|
|
bdea9e1333 | ||
|
|
2f8d66ef2b | ||
|
|
589b23b7e2 | ||
|
|
2e5131bb21 | ||
|
|
2ff5b4ce95 | ||
|
|
f8a478946e | ||
|
|
623f6c841b | ||
|
|
0b6f2f5b91 | ||
|
|
3445dc1f00 | ||
|
|
a11c2a0bd8 | ||
|
|
95da41aa15 | ||
|
|
27401f4975 | ||
|
|
d902a9f279 | ||
|
|
03847e6c41 | ||
|
|
a4f9602405 | ||
|
|
5f214ffa98 | ||
|
|
8ee3b535ef | ||
|
|
951372491c | ||
|
|
eeb79476de | ||
|
|
1b2d0e7a6f | ||
|
|
3208ad27ac | ||
|
|
cf87b76b0c | ||
|
|
5e71f0f0d7 | ||
|
|
be61e2e714 | ||
|
|
1e5596b594 | ||
|
|
744c277123 | ||
|
|
460bb69ade | ||
|
|
8dbe78a21a | ||
|
|
3959f82030 | ||
|
|
48ba13bc6c | ||
|
|
681082a3ad | ||
|
|
4013a90f33 | ||
|
|
316ef89541 | ||
|
|
a8dd81e986 | ||
|
|
4b257c3d01 | ||
|
|
491bc006b2 | ||
|
|
28ad0017e1 | ||
|
|
5849381dfb | ||
|
|
baa974a487 | ||
|
|
1a97ba1b46 | ||
|
|
1d68f4e279 | ||
|
|
a2b793c61b | ||
|
|
93d6fb8c60 | ||
|
|
c7f4bdafc0 | ||
|
|
867f80715e | ||
|
|
29e668e887 | ||
|
|
944f4f7c05 | ||
|
|
cd6544d32a | ||
|
|
b2f4bbf93b | ||
|
|
a99b4472a8 | ||
|
|
33f3e72dda | ||
|
|
e30510a688 | ||
|
|
974fe4d923 | ||
|
|
feb8aff46b | ||
|
|
eee9b50b70 | ||
|
|
9fb8bc8991 | ||
|
|
1c42caba76 | ||
|
|
9d59bfbe00 | ||
|
|
95dc06cca6 | ||
|
|
9ecbf86fa0 | ||
|
|
588fd1923f | ||
|
|
2824efd505 | ||
|
|
169c8d793a | ||
|
|
68f03dcc67 | ||
|
|
397f551e6d | ||
|
|
cbb5d34167 | ||
|
|
0cc9798c8f | ||
|
|
45a7ca62ae | ||
|
|
2eb125e90e | ||
|
|
264c618b11 | ||
|
|
d9cf8fcfe8 | ||
|
|
fbeaa57604 | ||
|
|
c1f5ead61d | ||
|
|
d7690c5fda | ||
|
|
45c35ceb2b | ||
|
|
bc481fa366 | ||
|
|
1b94fe3613 | ||
|
|
3204501174 | ||
|
|
f3dfc433c2 | ||
|
|
8213b1476f | ||
|
|
4e7dbf9ce5 | ||
|
|
ea2ff6aae3 | ||
|
|
50b6c5948d | ||
|
|
3acbd5a769 | ||
|
|
fddfb9e412 | ||
|
|
1325682d82 | ||
|
|
140a874917 | ||
|
|
b7c336a687 | ||
|
|
a38c0d6d15 | ||
|
|
75f40ccb06 | ||
|
|
4de847f84e | ||
|
|
33f1577dac | ||
|
|
ef3a83048c | ||
|
|
ae2ee8f006 | ||
|
|
6f6d86c700 | ||
|
|
d1b16e287c | ||
|
|
ee8a815e6b | ||
|
|
7bc2362e33 | ||
|
|
9a8389060c | ||
|
|
5cf9cd686c | ||
|
|
3341c5cf21 | ||
|
|
f1286f8e6b | ||
|
|
f2a99e83cd | ||
|
|
2f7b79764a | ||
|
|
ea18e06b08 | ||
|
|
a0193e8e42 | ||
|
|
2fcacbff23 | ||
|
|
a42288d056 | ||
|
|
33ee91a748 | ||
|
|
396895d077 | ||
|
|
8b04d48ffd | ||
|
|
2a76a0852f | ||
|
|
22d961de70 | ||
|
|
4650366f07 | ||
|
|
dfe17491f8 | ||
|
|
a8c7425e17 | ||
|
|
e5f0da75e2 | ||
|
|
6834e00be6 | ||
|
|
26375a3014 | ||
|
|
06c3f756b1 | ||
|
|
9c5bbfe96d | ||
|
|
e427f9ee38 | ||
|
|
e62e2bb131 | ||
|
|
bf17ed0917 | ||
|
|
058081b1f5 | ||
|
|
98722e10fc | ||
|
|
2781796d9c | ||
|
|
24d2261060 | ||
|
|
7d7c2104ea | ||
|
|
4ab502a691 | ||
|
|
9292d9255c | ||
|
|
2022d39339 | ||
|
|
e31dd4404e | ||
|
|
d2f4bce6c0 | ||
|
|
b0a3207454 | ||
|
|
db3cdb288e | ||
|
|
8797cb78a9 | ||
|
|
7eb5cd1267 | ||
|
|
0b2aff61bb | ||
|
|
ad4cba70a0 | ||
|
|
dd7890c848 |
17
.coveragerc
17
.coveragerc
@@ -215,6 +215,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
|
||||
|
||||
@@ -251,6 +254,9 @@ omit =
|
||||
homeassistant/components/scsgate.py
|
||||
homeassistant/components/*/scsgate.py
|
||||
|
||||
homeassistant/components/sisyphus.py
|
||||
homeassistant/components/*/sisyphus.py
|
||||
|
||||
homeassistant/components/skybell.py
|
||||
homeassistant/components/*/skybell.py
|
||||
|
||||
@@ -346,6 +352,9 @@ omit =
|
||||
homeassistant/components/tuya.py
|
||||
homeassistant/components/*/tuya.py
|
||||
|
||||
homeassistant/components/spider.py
|
||||
homeassistant/components/*/spider.py
|
||||
|
||||
homeassistant/components/alarm_control_panel/alarmdotcom.py
|
||||
homeassistant/components/alarm_control_panel/canary.py
|
||||
homeassistant/components/alarm_control_panel/concord232.py
|
||||
@@ -398,6 +407,8 @@ omit =
|
||||
homeassistant/components/climate/touchline.py
|
||||
homeassistant/components/climate/venstar.py
|
||||
homeassistant/components/climate/zhong_hong.py
|
||||
homeassistant/components/cover/aladdin_connect.py
|
||||
homeassistant/components/cover/brunt.py
|
||||
homeassistant/components/cover/garadget.py
|
||||
homeassistant/components/cover/gogogate2.py
|
||||
homeassistant/components/cover/homematic.py
|
||||
@@ -432,6 +443,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
|
||||
@@ -461,6 +473,7 @@ omit =
|
||||
homeassistant/components/light/decora_wifi.py
|
||||
homeassistant/components/light/decora.py
|
||||
homeassistant/components/light/flux_led.py
|
||||
homeassistant/components/light/futurenow.py
|
||||
homeassistant/components/light/greenwave.py
|
||||
homeassistant/components/light/hue.py
|
||||
homeassistant/components/light/hyperion.py
|
||||
@@ -500,6 +513,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
|
||||
@@ -523,6 +537,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
|
||||
@@ -623,6 +638,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
|
||||
@@ -657,6 +673,7 @@ omit =
|
||||
homeassistant/components/sensor/loopenergy.py
|
||||
homeassistant/components/sensor/luftdaten.py
|
||||
homeassistant/components/sensor/lyft.py
|
||||
homeassistant/components/sensor/magicseaweed.py
|
||||
homeassistant/components/sensor/metoffice.py
|
||||
homeassistant/components/sensor/miflora.py
|
||||
homeassistant/components/sensor/mitemp_bt.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
|
||||
|
||||
@@ -98,6 +98,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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Contributing to Home Assistant
|
||||
|
||||
Everybody is invited and welcome to contribute to Home Assistant. There is a lot to do...if you are not a developer perhaps you would like to help with the documentation on [home-assistant.io](https://home-assistant.io/)? If you are a developer and have devices in your home which aren't working with Home Assistant yet, why not spent a couple of hours and help to integrate them?
|
||||
Everybody is invited and welcome to contribute to Home Assistant. There is a lot to do...if you are not a developer perhaps you would like to help with the documentation on [home-assistant.io](https://home-assistant.io/)? If you are a developer and have devices in your home which aren't working with Home Assistant yet, why not spend a couple of hours and help to integrate them?
|
||||
|
||||
The process is straight-forward.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -8,7 +8,7 @@ import subprocess
|
||||
import sys
|
||||
import threading
|
||||
|
||||
from typing import Optional, List, Dict, Any # noqa #pylint: disable=unused-import
|
||||
from typing import List, Dict, Any # noqa pylint: disable=unused-import
|
||||
|
||||
|
||||
from homeassistant import monkey_patch
|
||||
@@ -20,7 +20,7 @@ from homeassistant.const import (
|
||||
)
|
||||
|
||||
|
||||
def attempt_use_uvloop():
|
||||
def attempt_use_uvloop() -> None:
|
||||
"""Attempt to use uvloop."""
|
||||
import asyncio
|
||||
|
||||
@@ -280,11 +280,11 @@ def setup_and_run_hass(config_dir: str,
|
||||
# Imported here to avoid importing asyncio before monkey patch
|
||||
from homeassistant.util.async_ import run_callback_threadsafe
|
||||
|
||||
def open_browser(event):
|
||||
"""Open the webinterface in a browser."""
|
||||
if hass.config.api is not None:
|
||||
def open_browser(_: Any) -> None:
|
||||
"""Open the web interface in a browser."""
|
||||
if hass.config.api is not None: # type: ignore
|
||||
import webbrowser
|
||||
webbrowser.open(hass.config.api.base_url)
|
||||
webbrowser.open(hass.config.api.base_url) # type: ignore
|
||||
|
||||
run_callback_threadsafe(
|
||||
hass.loop,
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
import asyncio
|
||||
import logging
|
||||
from collections import OrderedDict
|
||||
from typing import List, Awaitable
|
||||
|
||||
from homeassistant import data_entry_flow
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.core import callback, HomeAssistant
|
||||
|
||||
from . import models
|
||||
from . import auth_store
|
||||
@@ -13,7 +14,9 @@ from .providers import auth_provider_from_config
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def auth_manager_from_config(hass, provider_configs):
|
||||
async def auth_manager_from_config(
|
||||
hass: HomeAssistant,
|
||||
provider_configs: List[dict]) -> Awaitable['AuthManager']:
|
||||
"""Initialize an auth manager from config."""
|
||||
store = auth_store.AuthStore(hass)
|
||||
if provider_configs:
|
||||
@@ -208,7 +211,7 @@ class AuthManager:
|
||||
|
||||
return tkn
|
||||
|
||||
async def _async_create_login_flow(self, handler, *, source, data):
|
||||
async def _async_create_login_flow(self, handler, *, context, data):
|
||||
"""Create a login flow."""
|
||||
auth_provider = self._providers[handler]
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import base64
|
||||
from collections import OrderedDict
|
||||
import hashlib
|
||||
import hmac
|
||||
from typing import Dict # noqa: F401 pylint: disable=unused-import
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -68,12 +69,12 @@ class Data:
|
||||
"""Return users."""
|
||||
return self._data['users']
|
||||
|
||||
def validate_login(self, username, password):
|
||||
def validate_login(self, username: str, password: str) -> None:
|
||||
"""Validate a username and password.
|
||||
|
||||
Raises InvalidAuth if auth invalid.
|
||||
"""
|
||||
password = self.hash_password(password)
|
||||
hashed = self.hash_password(password)
|
||||
|
||||
found = None
|
||||
|
||||
@@ -84,33 +85,33 @@ class Data:
|
||||
|
||||
if found is None:
|
||||
# Do one more compare to make timing the same as if user was found.
|
||||
hmac.compare_digest(password, password)
|
||||
hmac.compare_digest(hashed, hashed)
|
||||
raise InvalidAuth
|
||||
|
||||
if not hmac.compare_digest(password,
|
||||
if not hmac.compare_digest(hashed,
|
||||
base64.b64decode(found['password'])):
|
||||
raise InvalidAuth
|
||||
|
||||
def hash_password(self, password, for_storage=False):
|
||||
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)
|
||||
if for_storage:
|
||||
hashed = base64.b64encode(hashed).decode()
|
||||
hashed = base64.b64encode(hashed)
|
||||
return hashed
|
||||
|
||||
def add_auth(self, username, password):
|
||||
def add_auth(self, username: str, password: str) -> None:
|
||||
"""Add a new authenticated user/pass."""
|
||||
if any(user['username'] == username for user in self.users):
|
||||
raise InvalidUser
|
||||
|
||||
self.users.append({
|
||||
'username': username,
|
||||
'password': self.hash_password(password, True),
|
||||
'password': self.hash_password(password, True).decode(),
|
||||
})
|
||||
|
||||
@callback
|
||||
def async_remove_auth(self, username):
|
||||
def async_remove_auth(self, username: str) -> None:
|
||||
"""Remove authentication."""
|
||||
index = None
|
||||
for i, user in enumerate(self.users):
|
||||
@@ -123,14 +124,15 @@ class Data:
|
||||
|
||||
self.users.pop(index)
|
||||
|
||||
def change_password(self, username, new_password):
|
||||
def change_password(self, username: str, new_password: str) -> None:
|
||||
"""Update the password.
|
||||
|
||||
Raises InvalidUser if user cannot be found.
|
||||
"""
|
||||
for user in self.users:
|
||||
if user['username'] == username:
|
||||
user['password'] = self.hash_password(new_password, True)
|
||||
user['password'] = self.hash_password(
|
||||
new_password, True).decode()
|
||||
break
|
||||
else:
|
||||
raise InvalidUser
|
||||
@@ -160,7 +162,7 @@ class HassAuthProvider(AuthProvider):
|
||||
"""Return a flow to login."""
|
||||
return LoginFlow(self)
|
||||
|
||||
async def async_validate_login(self, username, password):
|
||||
async def async_validate_login(self, username: str, password: str):
|
||||
"""Helper to validate a username and password."""
|
||||
if self.data is None:
|
||||
await self.async_initialize()
|
||||
@@ -225,7 +227,7 @@ class LoginFlow(data_entry_flow.FlowHandler):
|
||||
data=user_input
|
||||
)
|
||||
|
||||
schema = OrderedDict()
|
||||
schema = OrderedDict() # type: Dict[str, type]
|
||||
schema['username'] = str
|
||||
schema['password'] = str
|
||||
|
||||
|
||||
@@ -221,8 +221,8 @@ async def async_from_config_file(config_path: str,
|
||||
@core.callback
|
||||
def async_enable_logging(hass: core.HomeAssistant,
|
||||
verbose: bool = False,
|
||||
log_rotate_days=None,
|
||||
log_file=None,
|
||||
log_rotate_days: Optional[int] = None,
|
||||
log_file: Optional[str] = None,
|
||||
log_no_color: bool = False) -> None:
|
||||
"""Set up the logging.
|
||||
|
||||
@@ -291,9 +291,9 @@ def async_enable_logging(hass: core.HomeAssistant,
|
||||
|
||||
async_handler = AsyncHandler(hass.loop, err_handler)
|
||||
|
||||
async def async_stop_async_handler(event):
|
||||
async def async_stop_async_handler(_: Any) -> None:
|
||||
"""Cleanup async handler."""
|
||||
logging.getLogger('').removeHandler(async_handler)
|
||||
logging.getLogger('').removeHandler(async_handler) # type: ignore
|
||||
await async_handler.async_close(blocking=True)
|
||||
|
||||
hass.bus.async_listen_once(
|
||||
|
||||
@@ -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):
|
||||
@@ -167,7 +168,7 @@ def async_setup(hass, config):
|
||||
def async_handle_core_service(call):
|
||||
"""Service handler for handling core services."""
|
||||
if call.service == SERVICE_HOMEASSISTANT_STOP:
|
||||
hass.async_add_job(hass.async_stop())
|
||||
hass.async_create_task(hass.async_stop())
|
||||
return
|
||||
|
||||
try:
|
||||
@@ -183,7 +184,7 @@ def async_setup(hass, config):
|
||||
return
|
||||
|
||||
if call.service == SERVICE_HOMEASSISTANT_RESTART:
|
||||
hass.async_add_job(hass.async_stop(RESTART_EXIT_CODE))
|
||||
hass.async_create_task(hass.async_stop(RESTART_EXIT_CODE))
|
||||
|
||||
hass.services.async_register(
|
||||
ha.DOMAIN, SERVICE_HOMEASSISTANT_STOP, async_handle_core_service)
|
||||
|
||||
@@ -85,7 +85,7 @@ ABODE_PLATFORMS = [
|
||||
]
|
||||
|
||||
|
||||
class AbodeSystem(object):
|
||||
class AbodeSystem:
|
||||
"""Abode System class."""
|
||||
|
||||
def __init__(self, username, password, cache,
|
||||
|
||||
@@ -110,7 +110,7 @@ NotificationItem = namedtuple(
|
||||
)
|
||||
|
||||
|
||||
class AdsHub(object):
|
||||
class AdsHub:
|
||||
"""Representation of an ADS connection."""
|
||||
|
||||
def __init__(self, ads_client):
|
||||
|
||||
@@ -187,7 +187,7 @@ class AlarmControlPanel(Entity):
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.async_add_job(self.alarm_disarm, code)
|
||||
return self.hass.async_add_executor_job(self.alarm_disarm, code)
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
@@ -198,7 +198,7 @@ class AlarmControlPanel(Entity):
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.async_add_job(self.alarm_arm_home, code)
|
||||
return self.hass.async_add_executor_job(self.alarm_arm_home, code)
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
@@ -209,7 +209,7 @@ class AlarmControlPanel(Entity):
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.async_add_job(self.alarm_arm_away, code)
|
||||
return self.hass.async_add_executor_job(self.alarm_arm_away, code)
|
||||
|
||||
def alarm_arm_night(self, code=None):
|
||||
"""Send arm night command."""
|
||||
@@ -220,7 +220,7 @@ class AlarmControlPanel(Entity):
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.async_add_job(self.alarm_arm_night, code)
|
||||
return self.hass.async_add_executor_job(self.alarm_arm_night, code)
|
||||
|
||||
def alarm_trigger(self, code=None):
|
||||
"""Send alarm trigger command."""
|
||||
@@ -231,7 +231,7 @@ class AlarmControlPanel(Entity):
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.async_add_job(self.alarm_trigger, code)
|
||||
return self.hass.async_add_executor_job(self.alarm_trigger, code)
|
||||
|
||||
def alarm_arm_custom_bypass(self, code=None):
|
||||
"""Send arm custom bypass command."""
|
||||
@@ -242,7 +242,8 @@ class AlarmControlPanel(Entity):
|
||||
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.hass.async_add_job(self.alarm_arm_custom_bypass, code)
|
||||
return self.hass.async_add_executor_job(
|
||||
self.alarm_arm_custom_bypass, code)
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
|
||||
@@ -83,7 +83,7 @@ class AlarmDotCom(alarm.AlarmControlPanel):
|
||||
"""Return one or more digits/characters."""
|
||||
if self._code is None:
|
||||
return None
|
||||
elif isinstance(self._code, str) and re.search('^\\d+$', self._code):
|
||||
if isinstance(self._code, str) and re.search('^\\d+$', self._code):
|
||||
return 'Number'
|
||||
return 'Any'
|
||||
|
||||
@@ -92,9 +92,9 @@ class AlarmDotCom(alarm.AlarmControlPanel):
|
||||
"""Return the state of the device."""
|
||||
if self._alarm.state.lower() == 'disarmed':
|
||||
return STATE_ALARM_DISARMED
|
||||
elif self._alarm.state.lower() == 'armed stay':
|
||||
if self._alarm.state.lower() == 'armed stay':
|
||||
return STATE_ALARM_ARMED_HOME
|
||||
elif self._alarm.state.lower() == 'armed away':
|
||||
if self._alarm.state.lower() == 'armed away':
|
||||
return STATE_ALARM_ARMED_AWAY
|
||||
return STATE_UNKNOWN
|
||||
|
||||
|
||||
@@ -122,10 +122,10 @@ class ArloBaseStation(AlarmControlPanel):
|
||||
"""Convert Arlo mode to Home Assistant state."""
|
||||
if mode == ARMED:
|
||||
return STATE_ALARM_ARMED_AWAY
|
||||
elif mode == DISARMED:
|
||||
if mode == DISARMED:
|
||||
return STATE_ALARM_DISARMED
|
||||
elif mode == self._home_mode_name:
|
||||
if mode == self._home_mode_name:
|
||||
return STATE_ALARM_ARMED_HOME
|
||||
elif mode == self._away_mode_name:
|
||||
if mode == self._away_mode_name:
|
||||
return STATE_ALARM_ARMED_AWAY
|
||||
return mode
|
||||
|
||||
@@ -55,9 +55,9 @@ class CanaryAlarm(AlarmControlPanel):
|
||||
mode = location.mode
|
||||
if mode.name == LOCATION_MODE_AWAY:
|
||||
return STATE_ALARM_ARMED_AWAY
|
||||
elif mode.name == LOCATION_MODE_HOME:
|
||||
if mode.name == LOCATION_MODE_HOME:
|
||||
return STATE_ALARM_ARMED_HOME
|
||||
elif mode.name == LOCATION_MODE_NIGHT:
|
||||
if mode.name == LOCATION_MODE_NIGHT:
|
||||
return STATE_ALARM_ARMED_NIGHT
|
||||
return None
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/demo/
|
||||
"""
|
||||
import datetime
|
||||
import homeassistant.components.alarm_control_panel.manual as manual
|
||||
from homeassistant.components.alarm_control_panel import manual
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_CUSTOM_BYPASS,
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT,
|
||||
|
||||
@@ -20,7 +20,6 @@ DEPENDENCIES = ['homematicip_cloud']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
HMIP_OPEN = 'OPEN'
|
||||
HMIP_ZONE_AWAY = 'EXTERNAL'
|
||||
HMIP_ZONE_HOME = 'INTERNAL'
|
||||
|
||||
@@ -57,14 +56,18 @@ class HomematicipSecurityZone(HomematicipGenericDevice, AlarmControlPanel):
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
from homematicip.base.enums import WindowState
|
||||
|
||||
if self._device.active:
|
||||
if (self._device.sabotage or self._device.motionDetected or
|
||||
self._device.windowState == HMIP_OPEN):
|
||||
self._device.windowState == WindowState.OPEN):
|
||||
return STATE_ALARM_TRIGGERED
|
||||
|
||||
if self._device.label == HMIP_ZONE_HOME:
|
||||
active = self._home.get_security_zones_activation()
|
||||
if active == (True, True):
|
||||
return STATE_ALARM_ARMED_AWAY
|
||||
if active == (False, True):
|
||||
return STATE_ALARM_ARMED_HOME
|
||||
return STATE_ALARM_ARMED_AWAY
|
||||
|
||||
return STATE_ALARM_DISARMED
|
||||
|
||||
@@ -79,10 +82,3 @@ class HomematicipSecurityZone(HomematicipGenericDevice, AlarmControlPanel):
|
||||
async def async_alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
await self._home.set_security_zones_activation(True, True)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes of the alarm control device."""
|
||||
# The base class is loading the battery property, but device doesn't
|
||||
# have this property - base class needs clean-up.
|
||||
return None
|
||||
|
||||
@@ -128,7 +128,7 @@ class IFTTTAlarmPanel(alarm.AlarmControlPanel):
|
||||
"""Return one or more digits/characters."""
|
||||
if self._code is None:
|
||||
return None
|
||||
elif isinstance(self._code, str) and re.search('^\\d+$', self._code):
|
||||
if isinstance(self._code, str) and re.search('^\\d+$', self._code):
|
||||
return 'Number'
|
||||
return 'Any'
|
||||
|
||||
|
||||
@@ -205,7 +205,7 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
||||
"""Return one or more digits/characters."""
|
||||
if self._code is None:
|
||||
return None
|
||||
elif isinstance(self._code, str) and re.search('^\\d+$', self._code):
|
||||
if isinstance(self._code, str) and re.search('^\\d+$', self._code):
|
||||
return 'Number'
|
||||
return 'Any'
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ from homeassistant.const import (
|
||||
STATE_ALARM_DISARMED, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED,
|
||||
CONF_PLATFORM, CONF_NAME, CONF_CODE, CONF_DELAY_TIME, CONF_PENDING_TIME,
|
||||
CONF_TRIGGER_TIME, CONF_DISARM_AFTER_TRIGGER)
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
from homeassistant.components import mqtt
|
||||
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
from homeassistant.core import callback
|
||||
@@ -241,7 +241,7 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
|
||||
"""Return one or more digits/characters."""
|
||||
if self._code is None:
|
||||
return None
|
||||
elif isinstance(self._code, str) and re.search('^\\d+$', self._code):
|
||||
if isinstance(self._code, str) and re.search('^\\d+$', self._code):
|
||||
return 'Number'
|
||||
return 'Any'
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
from homeassistant.components import mqtt
|
||||
from homeassistant.const import (
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
|
||||
STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, STATE_UNKNOWN,
|
||||
@@ -49,6 +49,9 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, 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(
|
||||
config.get(CONF_NAME),
|
||||
config.get(CONF_STATE_TOPIC),
|
||||
@@ -123,7 +126,7 @@ class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel):
|
||||
"""Return one or more digits/characters."""
|
||||
if self._code is None:
|
||||
return None
|
||||
elif isinstance(self._code, str) and re.search('^\\d+$', self._code):
|
||||
if isinstance(self._code, str) and re.search('^\\d+$', self._code):
|
||||
return 'Number'
|
||||
return 'Any'
|
||||
|
||||
|
||||
@@ -9,23 +9,22 @@ import re
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
|
||||
from homeassistant.components.alarm_control_panel import (
|
||||
PLATFORM_SCHEMA, AlarmControlPanel)
|
||||
from homeassistant.const import (
|
||||
CONF_CODE, CONF_NAME, CONF_PASSWORD, CONF_USERNAME,
|
||||
EVENT_HOMEASSISTANT_STOP, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_DISARMED, STATE_UNKNOWN)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['simplisafe-python==1.0.5']
|
||||
REQUIREMENTS = ['simplisafe-python==2.0.2']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = 'SimpliSafe'
|
||||
DOMAIN = 'simplisafe'
|
||||
|
||||
NOTIFICATION_ID = 'simplisafe_notification'
|
||||
NOTIFICATION_TITLE = 'SimpliSafe Setup'
|
||||
ATTR_ALARM_ACTIVE = "alarm_active"
|
||||
ATTR_TEMPERATURE = "temperature"
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
@@ -37,36 +36,27 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the SimpliSafe platform."""
|
||||
from simplipy.api import SimpliSafeApiInterface, get_systems
|
||||
from simplipy.api import SimpliSafeApiInterface, SimpliSafeAPIException
|
||||
name = config.get(CONF_NAME)
|
||||
code = config.get(CONF_CODE)
|
||||
username = config.get(CONF_USERNAME)
|
||||
password = config.get(CONF_PASSWORD)
|
||||
|
||||
simplisafe = SimpliSafeApiInterface()
|
||||
status = simplisafe.set_credentials(username, password)
|
||||
if status:
|
||||
hass.data[DOMAIN] = simplisafe
|
||||
locations = get_systems(simplisafe)
|
||||
for location in locations:
|
||||
add_devices([SimpliSafeAlarm(location, name, code)])
|
||||
else:
|
||||
message = 'Failed to log into SimpliSafe. Check credentials.'
|
||||
_LOGGER.error(message)
|
||||
hass.components.persistent_notification.create(
|
||||
message,
|
||||
title=NOTIFICATION_TITLE,
|
||||
notification_id=NOTIFICATION_ID)
|
||||
return False
|
||||
try:
|
||||
simplisafe = SimpliSafeApiInterface(username, password)
|
||||
except SimpliSafeAPIException:
|
||||
_LOGGER.error("Failed to setup SimpliSafe")
|
||||
return
|
||||
|
||||
def logout(event):
|
||||
"""Logout of the SimpliSafe API."""
|
||||
hass.data[DOMAIN].logout()
|
||||
systems = []
|
||||
|
||||
hass.bus.listen(EVENT_HOMEASSISTANT_STOP, logout)
|
||||
for system in simplisafe.get_systems():
|
||||
systems.append(SimpliSafeAlarm(system, name, code))
|
||||
|
||||
add_devices(systems)
|
||||
|
||||
|
||||
class SimpliSafeAlarm(alarm.AlarmControlPanel):
|
||||
class SimpliSafeAlarm(AlarmControlPanel):
|
||||
"""Representation of a SimpliSafe alarm."""
|
||||
|
||||
def __init__(self, simplisafe, name, code):
|
||||
@@ -75,31 +65,37 @@ class SimpliSafeAlarm(alarm.AlarmControlPanel):
|
||||
self._name = name
|
||||
self._code = str(code) if code else None
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the unique ID."""
|
||||
return self.simplisafe.location_id
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
if self._name is not None:
|
||||
return self._name
|
||||
return 'Alarm {}'.format(self.simplisafe.location_id())
|
||||
return 'Alarm {}'.format(self.simplisafe.location_id)
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
"""Return one or more digits/characters."""
|
||||
if self._code is None:
|
||||
return None
|
||||
elif isinstance(self._code, str) and re.search('^\\d+$', self._code):
|
||||
if isinstance(self._code, str) and re.search('^\\d+$', self._code):
|
||||
return 'Number'
|
||||
return 'Any'
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
status = self.simplisafe.state()
|
||||
if status == 'off':
|
||||
status = self.simplisafe.state
|
||||
if status.lower() == 'off':
|
||||
state = STATE_ALARM_DISARMED
|
||||
elif status == 'home':
|
||||
elif status.lower() == 'home' or status.lower() == 'home_count':
|
||||
state = STATE_ALARM_ARMED_HOME
|
||||
elif status == 'away':
|
||||
elif (status.lower() == 'away' or status.lower() == 'exitDelay' or
|
||||
status.lower() == 'away_count'):
|
||||
state = STATE_ALARM_ARMED_AWAY
|
||||
else:
|
||||
state = STATE_UNKNOWN
|
||||
@@ -108,14 +104,13 @@ class SimpliSafeAlarm(alarm.AlarmControlPanel):
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return {
|
||||
'alarm': self.simplisafe.alarm(),
|
||||
'co': self.simplisafe.carbon_monoxide(),
|
||||
'fire': self.simplisafe.fire(),
|
||||
'flood': self.simplisafe.flood(),
|
||||
'last_event': self.simplisafe.last_event(),
|
||||
'temperature': self.simplisafe.temperature(),
|
||||
}
|
||||
attributes = {}
|
||||
|
||||
attributes[ATTR_ALARM_ACTIVE] = self.simplisafe.alarm_active
|
||||
if self.simplisafe.temperature is not None:
|
||||
attributes[ATTR_TEMPERATURE] = self.simplisafe.temperature
|
||||
|
||||
return attributes
|
||||
|
||||
def update(self):
|
||||
"""Update alarm status."""
|
||||
|
||||
@@ -34,6 +34,8 @@ CONF_ZONE_NAME = 'name'
|
||||
CONF_ZONE_TYPE = 'type'
|
||||
CONF_ZONE_RFID = 'rfid'
|
||||
CONF_ZONES = 'zones'
|
||||
CONF_RELAY_ADDR = 'relayaddr'
|
||||
CONF_RELAY_CHAN = 'relaychan'
|
||||
|
||||
DEFAULT_DEVICE_TYPE = 'socket'
|
||||
DEFAULT_DEVICE_HOST = 'localhost'
|
||||
@@ -53,6 +55,7 @@ SIGNAL_PANEL_DISARM = 'alarmdecoder.panel_disarm'
|
||||
SIGNAL_ZONE_FAULT = 'alarmdecoder.zone_fault'
|
||||
SIGNAL_ZONE_RESTORE = 'alarmdecoder.zone_restore'
|
||||
SIGNAL_RFX_MESSAGE = 'alarmdecoder.rfx_message'
|
||||
SIGNAL_REL_MESSAGE = 'alarmdecoder.rel_message'
|
||||
|
||||
DEVICE_SOCKET_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_DEVICE_TYPE): 'socket',
|
||||
@@ -71,7 +74,11 @@ ZONE_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_ZONE_NAME): cv.string,
|
||||
vol.Optional(CONF_ZONE_TYPE,
|
||||
default=DEFAULT_ZONE_TYPE): vol.Any(DEVICE_CLASSES_SCHEMA),
|
||||
vol.Optional(CONF_ZONE_RFID): cv.string})
|
||||
vol.Optional(CONF_ZONE_RFID): cv.string,
|
||||
vol.Inclusive(CONF_RELAY_ADDR, 'relaylocation',
|
||||
'Relay address and channel must exist together'): cv.byte,
|
||||
vol.Inclusive(CONF_RELAY_CHAN, 'relaylocation',
|
||||
'Relay address and channel must exist together'): cv.byte})
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
@@ -153,6 +160,11 @@ def setup(hass, config):
|
||||
hass.helpers.dispatcher.dispatcher_send(
|
||||
SIGNAL_ZONE_RESTORE, zone)
|
||||
|
||||
def handle_rel_message(sender, message):
|
||||
"""Handle relay message from AlarmDecoder."""
|
||||
hass.helpers.dispatcher.dispatcher_send(
|
||||
SIGNAL_REL_MESSAGE, message)
|
||||
|
||||
controller = False
|
||||
if device_type == 'socket':
|
||||
host = device.get(CONF_DEVICE_HOST)
|
||||
@@ -171,6 +183,7 @@ def setup(hass, config):
|
||||
controller.on_zone_fault += zone_fault_callback
|
||||
controller.on_zone_restore += zone_restore_callback
|
||||
controller.on_close += handle_closed_connection
|
||||
controller.on_relay_changed += handle_rel_message
|
||||
|
||||
hass.data[DATA_AD] = controller
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ def turn_on(hass, entity_id):
|
||||
def async_turn_on(hass, entity_id):
|
||||
"""Async reset the alert."""
|
||||
data = {ATTR_ENTITY_ID: entity_id}
|
||||
hass.async_add_job(
|
||||
hass.async_create_task(
|
||||
hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data))
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ def turn_off(hass, entity_id):
|
||||
def async_turn_off(hass, entity_id):
|
||||
"""Async acknowledge the alert."""
|
||||
data = {ATTR_ENTITY_ID: entity_id}
|
||||
hass.async_add_job(
|
||||
hass.async_create_task(
|
||||
hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, data))
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@ def toggle(hass, entity_id):
|
||||
def async_toggle(hass, entity_id):
|
||||
"""Async toggle acknowledgement of alert."""
|
||||
data = {ATTR_ENTITY_ID: entity_id}
|
||||
hass.async_add_job(
|
||||
hass.async_create_task(
|
||||
hass.services.async_call(DOMAIN, SERVICE_TOGGLE, data))
|
||||
|
||||
|
||||
@@ -217,7 +217,7 @@ class Alert(ToggleEntity):
|
||||
else:
|
||||
yield from self._schedule_notify()
|
||||
|
||||
self.hass.async_add_job(self.async_update_ha_state)
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def end_alerting(self):
|
||||
@@ -228,7 +228,7 @@ class Alert(ToggleEntity):
|
||||
self._firing = False
|
||||
if self._done_message and self._send_done_message:
|
||||
yield from self._notify_done_message()
|
||||
self.hass.async_add_job(self.async_update_ha_state)
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def _schedule_notify(self):
|
||||
|
||||
@@ -210,7 +210,7 @@ def resolve_slot_synonyms(key, request):
|
||||
return resolved_value
|
||||
|
||||
|
||||
class AlexaResponse(object):
|
||||
class AlexaResponse:
|
||||
"""Help generating the response for Alexa."""
|
||||
|
||||
def __init__(self, hass, intent_info):
|
||||
|
||||
@@ -55,7 +55,7 @@ HANDLERS = Registry()
|
||||
ENTITY_ADAPTERS = Registry()
|
||||
|
||||
|
||||
class _DisplayCategory(object):
|
||||
class _DisplayCategory:
|
||||
"""Possible display categories for Discovery response.
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/alexa-discovery.html#display-categories
|
||||
@@ -153,7 +153,7 @@ class _UnsupportedProperty(Exception):
|
||||
"""This entity does not support the requested Smart Home API property."""
|
||||
|
||||
|
||||
class _AlexaEntity(object):
|
||||
class _AlexaEntity:
|
||||
"""An adaptation of an entity, expressed in Alexa's terms.
|
||||
|
||||
The API handlers should manipulate entities only through this interface.
|
||||
@@ -208,7 +208,7 @@ class _AlexaEntity(object):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class _AlexaInterface(object):
|
||||
class _AlexaInterface:
|
||||
def __init__(self, entity):
|
||||
self.entity = entity
|
||||
|
||||
@@ -315,7 +315,7 @@ class _AlexaLockController(_AlexaInterface):
|
||||
|
||||
if self.entity.state == STATE_LOCKED:
|
||||
return 'LOCKED'
|
||||
elif self.entity.state == STATE_UNLOCKED:
|
||||
if self.entity.state == STATE_UNLOCKED:
|
||||
return 'UNLOCKED'
|
||||
return 'JAMMED'
|
||||
|
||||
@@ -615,7 +615,7 @@ class _SensorCapabilities(_AlexaEntity):
|
||||
yield _AlexaTemperatureSensor(self.entity)
|
||||
|
||||
|
||||
class _Cause(object):
|
||||
class _Cause:
|
||||
"""Possible causes for property changes.
|
||||
|
||||
https://developer.amazon.com/docs/smarthome/state-reporting-for-a-smart-home-skill.html#cause-object
|
||||
|
||||
@@ -164,7 +164,7 @@ def setup(hass, config):
|
||||
return True
|
||||
|
||||
|
||||
class AmcrestDevice(object):
|
||||
class AmcrestDevice:
|
||||
"""Representation of a base Amcrest discovery device."""
|
||||
|
||||
def __init__(self, camera, name, authentication, ffmpeg_arguments,
|
||||
|
||||
@@ -214,11 +214,11 @@ def async_setup(hass, config):
|
||||
CONF_PASSWORD: password
|
||||
})
|
||||
|
||||
hass.async_add_job(discovery.async_load_platform(
|
||||
hass.async_create_task(discovery.async_load_platform(
|
||||
hass, 'camera', 'mjpeg', mjpeg_camera, config))
|
||||
|
||||
if sensors:
|
||||
hass.async_add_job(discovery.async_load_platform(
|
||||
hass.async_create_task(discovery.async_load_platform(
|
||||
hass, 'sensor', DOMAIN, {
|
||||
CONF_NAME: name,
|
||||
CONF_HOST: host,
|
||||
@@ -226,7 +226,7 @@ def async_setup(hass, config):
|
||||
}, config))
|
||||
|
||||
if switches:
|
||||
hass.async_add_job(discovery.async_load_platform(
|
||||
hass.async_create_task(discovery.async_load_platform(
|
||||
hass, 'switch', DOMAIN, {
|
||||
CONF_NAME: name,
|
||||
CONF_HOST: host,
|
||||
@@ -234,7 +234,7 @@ def async_setup(hass, config):
|
||||
}, config))
|
||||
|
||||
if motion:
|
||||
hass.async_add_job(discovery.async_load_platform(
|
||||
hass.async_create_task(discovery.async_load_platform(
|
||||
hass, 'binary_sensor', DOMAIN, {
|
||||
CONF_HOST: host,
|
||||
CONF_NAME: name,
|
||||
|
||||
@@ -58,7 +58,7 @@ def setup(hass, config):
|
||||
return True
|
||||
|
||||
|
||||
class APCUPSdData(object):
|
||||
class APCUPSdData:
|
||||
"""Stores the data retrieved from APCUPSd.
|
||||
|
||||
For each entity to use, acts as the single point responsible for fetching
|
||||
|
||||
@@ -220,7 +220,8 @@ class APIEntityStateView(HomeAssistantView):
|
||||
is_new_state = hass.states.get(entity_id) is None
|
||||
|
||||
# Write state
|
||||
hass.states.async_set(entity_id, new_state, attributes, force_update)
|
||||
hass.states.async_set(entity_id, new_state, attributes, force_update,
|
||||
self.context(request))
|
||||
|
||||
# Read the state back for our response
|
||||
status_code = HTTP_CREATED if is_new_state else 200
|
||||
@@ -279,7 +280,8 @@ class APIEventView(HomeAssistantView):
|
||||
event_data[key] = state
|
||||
|
||||
request.app['hass'].bus.async_fire(
|
||||
event_type, event_data, ha.EventOrigin.remote)
|
||||
event_type, event_data, ha.EventOrigin.remote,
|
||||
self.context(request))
|
||||
|
||||
return self.json_message("Event {} fired.".format(event_type))
|
||||
|
||||
@@ -316,7 +318,8 @@ class APIDomainServicesView(HomeAssistantView):
|
||||
"Data should be valid JSON.", HTTP_BAD_REQUEST)
|
||||
|
||||
with AsyncTrackStates(hass) as changed_states:
|
||||
await hass.services.async_call(domain, service, data, True)
|
||||
await hass.services.async_call(
|
||||
domain, service, data, True, self.context(request))
|
||||
|
||||
return self.json(changed_states)
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ NOTIFICATION_AUTH_TITLE = 'Apple TV Authentication'
|
||||
NOTIFICATION_SCAN_ID = 'apple_tv_scan_notification'
|
||||
NOTIFICATION_SCAN_TITLE = 'Apple TV Scan'
|
||||
|
||||
T = TypeVar('T')
|
||||
T = TypeVar('T') # pylint: disable=invalid-name
|
||||
|
||||
|
||||
# This version of ensure_list interprets an empty dict as no value
|
||||
@@ -218,10 +218,10 @@ def _setup_atv(hass, atv_config):
|
||||
ATTR_POWER: power
|
||||
}
|
||||
|
||||
hass.async_add_job(discovery.async_load_platform(
|
||||
hass.async_create_task(discovery.async_load_platform(
|
||||
hass, 'media_player', DOMAIN, atv_config))
|
||||
|
||||
hass.async_add_job(discovery.async_load_platform(
|
||||
hass.async_create_task(discovery.async_load_platform(
|
||||
hass, 'remote', DOMAIN, atv_config))
|
||||
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ def setup(hass, config):
|
||||
return True
|
||||
|
||||
|
||||
class ArduinoBoard(object):
|
||||
class ArduinoBoard:
|
||||
"""Representation of an Arduino board."""
|
||||
|
||||
def __init__(self, port):
|
||||
|
||||
@@ -16,7 +16,7 @@ from homeassistant.const import (
|
||||
from homeassistant.helpers.event import track_time_interval
|
||||
from homeassistant.helpers.dispatcher import dispatcher_send
|
||||
|
||||
REQUIREMENTS = ['pyarlo==0.1.9']
|
||||
REQUIREMENTS = ['pyarlo==0.2.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ def setup(hass, config):
|
||||
return True
|
||||
|
||||
|
||||
class AsteriskData(object):
|
||||
class AsteriskData:
|
||||
"""Store Asterisk mailbox data."""
|
||||
|
||||
def __init__(self, hass, host, port, password):
|
||||
|
||||
@@ -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
|
||||
@@ -123,9 +123,9 @@ def setup_august(hass, config, api, authenticator):
|
||||
discovery.load_platform(hass, component, DOMAIN, {}, config)
|
||||
|
||||
return True
|
||||
elif state == AuthenticationState.BAD_PASSWORD:
|
||||
if state == AuthenticationState.BAD_PASSWORD:
|
||||
return False
|
||||
elif state == AuthenticationState.REQUIRES_VALIDATION:
|
||||
if state == AuthenticationState.REQUIRES_VALIDATION:
|
||||
request_configuration(hass, config, api, authenticator)
|
||||
return True
|
||||
|
||||
|
||||
@@ -1,62 +1,5 @@
|
||||
"""Component to allow users to login and get tokens.
|
||||
|
||||
All requests will require passing in a valid client ID and secret via HTTP
|
||||
Basic Auth.
|
||||
|
||||
# GET /auth/providers
|
||||
|
||||
Return a list of auth providers. Example:
|
||||
|
||||
[
|
||||
{
|
||||
"name": "Local",
|
||||
"id": null,
|
||||
"type": "local_provider",
|
||||
}
|
||||
]
|
||||
|
||||
# POST /auth/login_flow
|
||||
|
||||
Create a login flow. Will return the first step of the flow.
|
||||
|
||||
Pass in parameter 'handler' to specify the auth provider to use. Auth providers
|
||||
are identified by type and id.
|
||||
|
||||
{
|
||||
"handler": ["local_provider", null]
|
||||
}
|
||||
|
||||
Return value will be a step in a data entry flow. See the docs for data entry
|
||||
flow for details.
|
||||
|
||||
{
|
||||
"data_schema": [
|
||||
{"name": "username", "type": "string"},
|
||||
{"name": "password", "type": "string"}
|
||||
],
|
||||
"errors": {},
|
||||
"flow_id": "8f7e42faab604bcab7ac43c44ca34d58",
|
||||
"handler": ["insecure_example", null],
|
||||
"step_id": "init",
|
||||
"type": "form"
|
||||
}
|
||||
|
||||
# POST /auth/login_flow/{flow_id}
|
||||
|
||||
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.
|
||||
|
||||
{
|
||||
"flow_id": "8f7e42faab604bcab7ac43c44ca34d58",
|
||||
"handler": ["insecure_example", null],
|
||||
"result": "411ee2f916e648d691e937ae9344681e",
|
||||
"source": "user",
|
||||
"title": "Example",
|
||||
"type": "create_entry",
|
||||
"version": 1
|
||||
}
|
||||
|
||||
# POST /auth/token
|
||||
|
||||
This is an OAuth2 endpoint for granting tokens. We currently support the grant
|
||||
@@ -102,24 +45,20 @@ a limited expiration.
|
||||
"token_type": "Bearer"
|
||||
}
|
||||
"""
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import uuid
|
||||
from datetime import timedelta
|
||||
|
||||
import aiohttp.web
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import data_entry_flow
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.data_entry_flow import (
|
||||
FlowManagerIndexView, FlowManagerResourceView)
|
||||
from homeassistant.components import websocket_api
|
||||
from homeassistant.components.http.view import HomeAssistantView
|
||||
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.util import dt as dt_util
|
||||
|
||||
from . import indieauth
|
||||
|
||||
from . import login_flow
|
||||
|
||||
DOMAIN = 'auth'
|
||||
DEPENDENCIES = ['http']
|
||||
@@ -136,10 +75,6 @@ async def async_setup(hass, config):
|
||||
"""Component to allow users to login."""
|
||||
store_credentials, retrieve_credentials = _create_cred_store()
|
||||
|
||||
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))
|
||||
hass.http.register_view(GrantTokenView(retrieve_credentials))
|
||||
hass.http.register_view(LinkUserView(retrieve_credentials))
|
||||
|
||||
@@ -148,93 +83,11 @@ async def async_setup(hass, config):
|
||||
SCHEMA_WS_CURRENT_USER
|
||||
)
|
||||
|
||||
await login_flow.async_setup(hass, store_credentials)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class AuthProvidersView(HomeAssistantView):
|
||||
"""View to get available auth providers."""
|
||||
|
||||
url = '/auth/providers'
|
||||
name = 'api:auth:providers'
|
||||
requires_auth = False
|
||||
|
||||
async def get(self, request):
|
||||
"""Get available auth providers."""
|
||||
return self.json([{
|
||||
'name': provider.name,
|
||||
'id': provider.id,
|
||||
'type': provider.type,
|
||||
} for provider in request.app['hass'].auth.auth_providers])
|
||||
|
||||
|
||||
class LoginFlowIndexView(FlowManagerIndexView):
|
||||
"""View to create a config flow."""
|
||||
|
||||
url = '/auth/login_flow'
|
||||
name = 'api:auth:login_flow'
|
||||
requires_auth = False
|
||||
|
||||
async def get(self, request):
|
||||
"""Do not allow index of flows in progress."""
|
||||
return aiohttp.web.Response(status=405)
|
||||
|
||||
@RequestDataValidator(vol.Schema({
|
||||
vol.Required('client_id'): str,
|
||||
vol.Required('handler'): vol.Any(str, list),
|
||||
vol.Required('redirect_uri'): str,
|
||||
}))
|
||||
async def post(self, request, data):
|
||||
"""Create a new login flow."""
|
||||
if not indieauth.verify_redirect_uri(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)
|
||||
|
||||
|
||||
class LoginFlowResourceView(FlowManagerResourceView):
|
||||
"""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):
|
||||
"""Initialize the login flow resource view."""
|
||||
super().__init__(flow_mgr)
|
||||
self._store_credentials = store_credentials
|
||||
|
||||
async def get(self, request, flow_id):
|
||||
"""Do not allow getting status of a flow in progress."""
|
||||
return self.json_message('Invalid flow specified', 404)
|
||||
|
||||
@RequestDataValidator(vol.Schema({
|
||||
'client_id': str
|
||||
}, extra=vol.ALLOW_EXTRA))
|
||||
async def post(self, request, flow_id, data):
|
||||
"""Handle progressing a login flow request."""
|
||||
client_id = data.pop('client_id')
|
||||
|
||||
if not indieauth.verify_client_id(client_id):
|
||||
return self.json_message('Invalid client id', 400)
|
||||
|
||||
try:
|
||||
result = await self._flow_mgr.async_configure(flow_id, data)
|
||||
except data_entry_flow.UnknownFlow:
|
||||
return self.json_message('Invalid flow specified', 404)
|
||||
except vol.Invalid:
|
||||
return self.json_message('User input malformed', 400)
|
||||
|
||||
if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
|
||||
return self.json(self._prepare_result_json(result))
|
||||
|
||||
result.pop('data')
|
||||
result['result'] = self._store_credentials(client_id, result['result'])
|
||||
|
||||
return self.json(result)
|
||||
|
||||
|
||||
class GrantTokenView(HomeAssistantView):
|
||||
"""View to grant tokens."""
|
||||
|
||||
@@ -247,11 +100,26 @@ class GrantTokenView(HomeAssistantView):
|
||||
"""Initialize the grant token view."""
|
||||
self._retrieve_credentials = retrieve_credentials
|
||||
|
||||
@log_invalid_auth
|
||||
async def post(self, request):
|
||||
"""Grant a token."""
|
||||
hass = request.app['hass']
|
||||
data = await request.post()
|
||||
|
||||
grant_type = data.get('grant_type')
|
||||
|
||||
if grant_type == 'authorization_code':
|
||||
return await self._async_handle_auth_code(hass, data)
|
||||
|
||||
if grant_type == 'refresh_token':
|
||||
return await self._async_handle_refresh_token(hass, data)
|
||||
|
||||
return self.json({
|
||||
'error': 'unsupported_grant_type',
|
||||
}, status_code=400)
|
||||
|
||||
async def _async_handle_auth_code(self, hass, data):
|
||||
"""Handle authorization code request."""
|
||||
client_id = data.get('client_id')
|
||||
if client_id is None or not indieauth.verify_client_id(client_id):
|
||||
return self.json({
|
||||
@@ -259,21 +127,6 @@ class GrantTokenView(HomeAssistantView):
|
||||
'error_description': 'Invalid client id',
|
||||
}, status_code=400)
|
||||
|
||||
grant_type = data.get('grant_type')
|
||||
|
||||
if grant_type == 'authorization_code':
|
||||
return await self._async_handle_auth_code(hass, client_id, data)
|
||||
|
||||
elif grant_type == 'refresh_token':
|
||||
return await self._async_handle_refresh_token(
|
||||
hass, client_id, data)
|
||||
|
||||
return self.json({
|
||||
'error': 'unsupported_grant_type',
|
||||
}, status_code=400)
|
||||
|
||||
async def _async_handle_auth_code(self, hass, client_id, data):
|
||||
"""Handle authorization code request."""
|
||||
code = data.get('code')
|
||||
|
||||
if code is None:
|
||||
@@ -309,8 +162,15 @@ class GrantTokenView(HomeAssistantView):
|
||||
int(refresh_token.access_token_expiration.total_seconds()),
|
||||
})
|
||||
|
||||
async def _async_handle_refresh_token(self, hass, client_id, data):
|
||||
async def _async_handle_refresh_token(self, hass, data):
|
||||
"""Handle authorization code request."""
|
||||
client_id = data.get('client_id')
|
||||
if client_id is not None and not indieauth.verify_client_id(client_id):
|
||||
return self.json({
|
||||
'error': 'invalid_request',
|
||||
'error_description': 'Invalid client id',
|
||||
}, status_code=400)
|
||||
|
||||
token = data.get('refresh_token')
|
||||
|
||||
if token is None:
|
||||
@@ -320,11 +180,16 @@ class GrantTokenView(HomeAssistantView):
|
||||
|
||||
refresh_token = await hass.auth.async_get_refresh_token(token)
|
||||
|
||||
if refresh_token is None or refresh_token.client_id != client_id:
|
||||
if refresh_token is None:
|
||||
return self.json({
|
||||
'error': 'invalid_grant',
|
||||
}, status_code=400)
|
||||
|
||||
if refresh_token.client_id != client_id:
|
||||
return self.json({
|
||||
'error': 'invalid_request',
|
||||
}, status_code=400)
|
||||
|
||||
access_token = hass.auth.async_create_access_token(refresh_token)
|
||||
|
||||
return self.json({
|
||||
@@ -412,4 +277,7 @@ def websocket_current_user(hass, connection, msg):
|
||||
'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]
|
||||
}))
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
"""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
|
||||
|
||||
from aiohttp.client_exceptions import ClientError
|
||||
|
||||
# IP addresses of loopback interfaces
|
||||
ALLOWED_IPS = (
|
||||
@@ -16,7 +20,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 +29,75 @@ 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.
|
||||
"""
|
||||
session = hass.helpers.aiohttp_client.async_get_clientsession()
|
||||
parser = LinkTagParser('redirect_uri')
|
||||
chunks = 0
|
||||
try:
|
||||
resp = await session.get(url, timeout=5)
|
||||
|
||||
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."""
|
||||
|
||||
218
homeassistant/components/auth/login_flow.py
Normal file
218
homeassistant/components/auth/login_flow.py
Normal file
@@ -0,0 +1,218 @@
|
||||
"""HTTP views handle login flow.
|
||||
|
||||
# GET /auth/providers
|
||||
|
||||
Return a list of auth providers. Example:
|
||||
|
||||
[
|
||||
{
|
||||
"name": "Local",
|
||||
"id": null,
|
||||
"type": "local_provider",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
# POST /auth/login_flow
|
||||
|
||||
Create a login flow. Will return the first step of the flow.
|
||||
|
||||
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.
|
||||
|
||||
{
|
||||
"client_id": "https://hassbian.local:8123/",
|
||||
"handler": ["local_provider", null],
|
||||
"redirect_url": "https://hassbian.local:8123/"
|
||||
}
|
||||
|
||||
Return value will be a step in a data entry flow. See the docs for data entry
|
||||
flow for details.
|
||||
|
||||
{
|
||||
"data_schema": [
|
||||
{"name": "username", "type": "string"},
|
||||
{"name": "password", "type": "string"}
|
||||
],
|
||||
"errors": {},
|
||||
"flow_id": "8f7e42faab604bcab7ac43c44ca34d58",
|
||||
"handler": ["insecure_example", null],
|
||||
"step_id": "init",
|
||||
"type": "form"
|
||||
}
|
||||
|
||||
|
||||
# POST /auth/login_flow/{flow_id}
|
||||
|
||||
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.
|
||||
|
||||
{
|
||||
"flow_id": "8f7e42faab604bcab7ac43c44ca34d58",
|
||||
"handler": ["insecure_example", null],
|
||||
"result": "411ee2f916e648d691e937ae9344681e",
|
||||
"source": "user",
|
||||
"title": "Example",
|
||||
"type": "create_entry",
|
||||
"version": 1
|
||||
}
|
||||
"""
|
||||
import aiohttp.web
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import data_entry_flow
|
||||
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 . import indieauth
|
||||
|
||||
|
||||
async def async_setup(hass, store_credentials):
|
||||
"""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))
|
||||
|
||||
|
||||
class AuthProvidersView(HomeAssistantView):
|
||||
"""View to get available auth providers."""
|
||||
|
||||
url = '/auth/providers'
|
||||
name = 'api:auth:providers'
|
||||
requires_auth = False
|
||||
|
||||
async def get(self, request):
|
||||
"""Get available auth providers."""
|
||||
return self.json([{
|
||||
'name': provider.name,
|
||||
'id': provider.id,
|
||||
'type': provider.type,
|
||||
} for provider in request.app['hass'].auth.auth_providers])
|
||||
|
||||
|
||||
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)
|
||||
|
||||
@RequestDataValidator(vol.Schema({
|
||||
vol.Required('client_id'): str,
|
||||
vol.Required('handler'): vol.Any(str, list),
|
||||
vol.Required('redirect_uri'): str,
|
||||
}))
|
||||
@log_invalid_auth
|
||||
async def post(self, request, data):
|
||||
"""Create a new login flow."""
|
||||
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)
|
||||
|
||||
if isinstance(data['handler'], list):
|
||||
handler = tuple(data['handler'])
|
||||
else:
|
||||
handler = data['handler']
|
||||
|
||||
try:
|
||||
result = await self._flow_mgr.async_init(handler)
|
||||
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(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):
|
||||
"""Initialize the login flow resource view."""
|
||||
self._flow_mgr = flow_mgr
|
||||
self._store_credentials = store_credentials
|
||||
|
||||
async def get(self, request):
|
||||
"""Do not allow getting status of a flow in progress."""
|
||||
return self.json_message('Invalid flow specified', 404)
|
||||
|
||||
@RequestDataValidator(vol.Schema({
|
||||
'client_id': str
|
||||
}, extra=vol.ALLOW_EXTRA))
|
||||
@log_invalid_auth
|
||||
async def post(self, request, flow_id, data):
|
||||
"""Handle progressing a login flow request."""
|
||||
client_id = data.pop('client_id')
|
||||
|
||||
if not indieauth.verify_client_id(client_id):
|
||||
return self.json_message('Invalid client id', 400)
|
||||
|
||||
try:
|
||||
result = await self._flow_mgr.async_configure(flow_id, data)
|
||||
except data_entry_flow.UnknownFlow:
|
||||
return self.json_message('Invalid flow specified', 404)
|
||||
except vol.Invalid:
|
||||
return self.json_message('User input malformed', 400)
|
||||
|
||||
if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
|
||||
# @log_invalid_auth does not work here since it returns HTTP 200
|
||||
# need manually log failed login attempts
|
||||
if result['errors'] is not None and \
|
||||
result['errors'].get('base') == 'invalid_auth':
|
||||
await process_wrong_login(request)
|
||||
return self.json(_prepare_result_json(result))
|
||||
|
||||
result.pop('data')
|
||||
result['result'] = self._store_credentials(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')
|
||||
@@ -297,7 +297,7 @@ class AutomationEntity(ToggleEntity):
|
||||
return
|
||||
|
||||
# HomeAssistant is starting up
|
||||
elif self.hass.state == CoreState.not_running:
|
||||
if self.hass.state == CoreState.not_running:
|
||||
@asyncio.coroutine
|
||||
def async_enable_automation(event):
|
||||
"""Start automation on startup."""
|
||||
|
||||
@@ -44,7 +44,7 @@ def async_trigger(hass, config, action):
|
||||
|
||||
# Automation are enabled while hass is starting up, fire right away
|
||||
# Check state because a config reload shouldn't trigger it.
|
||||
elif hass.state == CoreState.starting:
|
||||
if hass.state == CoreState.starting:
|
||||
hass.async_run_job(action, {
|
||||
'trigger': {
|
||||
'platform': 'homeassistant',
|
||||
|
||||
@@ -10,7 +10,7 @@ import json
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
from homeassistant.components import mqtt
|
||||
from homeassistant.const import (CONF_PLATFORM, CONF_PAYLOAD)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ DOMAIN = 'bbb_gpio'
|
||||
def setup(hass, config):
|
||||
"""Set up the BeagleBone Black GPIO component."""
|
||||
# pylint: disable=import-error
|
||||
import Adafruit_BBIO.GPIO as GPIO
|
||||
from Adafruit_BBIO import GPIO
|
||||
|
||||
def cleanup_gpio(event):
|
||||
"""Stuff to do before stopping."""
|
||||
@@ -36,14 +36,14 @@ def setup(hass, config):
|
||||
def setup_output(pin):
|
||||
"""Set up a GPIO as output."""
|
||||
# pylint: disable=import-error
|
||||
import Adafruit_BBIO.GPIO as GPIO
|
||||
from Adafruit_BBIO import GPIO
|
||||
GPIO.setup(pin, GPIO.OUT)
|
||||
|
||||
|
||||
def setup_input(pin, pull_mode):
|
||||
"""Set up a GPIO as input."""
|
||||
# pylint: disable=import-error
|
||||
import Adafruit_BBIO.GPIO as GPIO
|
||||
from Adafruit_BBIO import GPIO
|
||||
GPIO.setup(pin, GPIO.IN,
|
||||
GPIO.PUD_DOWN if pull_mode == 'DOWN'
|
||||
else GPIO.PUD_UP)
|
||||
@@ -52,20 +52,20 @@ def setup_input(pin, pull_mode):
|
||||
def write_output(pin, value):
|
||||
"""Write a value to a GPIO."""
|
||||
# pylint: disable=import-error
|
||||
import Adafruit_BBIO.GPIO as GPIO
|
||||
from Adafruit_BBIO import GPIO
|
||||
GPIO.output(pin, value)
|
||||
|
||||
|
||||
def read_input(pin):
|
||||
"""Read a value from a GPIO."""
|
||||
# pylint: disable=import-error
|
||||
import Adafruit_BBIO.GPIO as GPIO
|
||||
from Adafruit_BBIO import GPIO
|
||||
return GPIO.input(pin) is GPIO.HIGH
|
||||
|
||||
|
||||
def edge_detect(pin, event_callback, bounce):
|
||||
"""Add detection for RISING and FALLING events."""
|
||||
# pylint: disable=import-error
|
||||
import Adafruit_BBIO.GPIO as GPIO
|
||||
from Adafruit_BBIO import GPIO
|
||||
GPIO.add_event_detect(
|
||||
pin, GPIO.BOTH, callback=event_callback, bouncetime=bounce)
|
||||
|
||||
@@ -11,7 +11,8 @@ from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.alarmdecoder import (
|
||||
ZONE_SCHEMA, CONF_ZONES, CONF_ZONE_NAME, CONF_ZONE_TYPE,
|
||||
CONF_ZONE_RFID, SIGNAL_ZONE_FAULT, SIGNAL_ZONE_RESTORE,
|
||||
SIGNAL_RFX_MESSAGE)
|
||||
SIGNAL_RFX_MESSAGE, SIGNAL_REL_MESSAGE, CONF_RELAY_ADDR,
|
||||
CONF_RELAY_CHAN)
|
||||
|
||||
DEPENDENCIES = ['alarmdecoder']
|
||||
|
||||
@@ -37,8 +38,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
zone_type = device_config_data[CONF_ZONE_TYPE]
|
||||
zone_name = device_config_data[CONF_ZONE_NAME]
|
||||
zone_rfid = device_config_data.get(CONF_ZONE_RFID)
|
||||
relay_addr = device_config_data.get(CONF_RELAY_ADDR)
|
||||
relay_chan = device_config_data.get(CONF_RELAY_CHAN)
|
||||
device = AlarmDecoderBinarySensor(
|
||||
zone_num, zone_name, zone_type, zone_rfid)
|
||||
zone_num, zone_name, zone_type, zone_rfid, relay_addr, relay_chan)
|
||||
devices.append(device)
|
||||
|
||||
add_devices(devices)
|
||||
@@ -49,7 +52,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
class AlarmDecoderBinarySensor(BinarySensorDevice):
|
||||
"""Representation of an AlarmDecoder binary sensor."""
|
||||
|
||||
def __init__(self, zone_number, zone_name, zone_type, zone_rfid):
|
||||
def __init__(self, zone_number, zone_name, zone_type, zone_rfid,
|
||||
relay_addr, relay_chan):
|
||||
"""Initialize the binary_sensor."""
|
||||
self._zone_number = zone_number
|
||||
self._zone_type = zone_type
|
||||
@@ -57,6 +61,8 @@ class AlarmDecoderBinarySensor(BinarySensorDevice):
|
||||
self._name = zone_name
|
||||
self._rfid = zone_rfid
|
||||
self._rfstate = None
|
||||
self._relay_addr = relay_addr
|
||||
self._relay_chan = relay_chan
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
@@ -70,6 +76,9 @@ class AlarmDecoderBinarySensor(BinarySensorDevice):
|
||||
self.hass.helpers.dispatcher.async_dispatcher_connect(
|
||||
SIGNAL_RFX_MESSAGE, self._rfx_message_callback)
|
||||
|
||||
self.hass.helpers.dispatcher.async_dispatcher_connect(
|
||||
SIGNAL_REL_MESSAGE, self._rel_message_callback)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the entity."""
|
||||
@@ -122,3 +131,12 @@ class AlarmDecoderBinarySensor(BinarySensorDevice):
|
||||
if self._rfid and message and message.serial_number == self._rfid:
|
||||
self._rfstate = message.value
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def _rel_message_callback(self, message):
|
||||
"""Update relay state."""
|
||||
if (self._relay_addr == message.address and
|
||||
self._relay_chan == message.channel):
|
||||
_LOGGER.debug("Relay %d:%d value:%d", message.address,
|
||||
message.channel, message.value)
|
||||
self._state = message.value
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@@ -89,7 +89,7 @@ class ArestBinarySensor(BinarySensorDevice):
|
||||
self.arest.update()
|
||||
|
||||
|
||||
class ArestData(object):
|
||||
class ArestData:
|
||||
"""Class for handling the data retrieval for pins."""
|
||||
|
||||
def __init__(self, resource, pin):
|
||||
|
||||
@@ -99,7 +99,7 @@ class AuroraSensor(BinarySensorDevice):
|
||||
self.aurora_data.update()
|
||||
|
||||
|
||||
class AuroraData(object):
|
||||
class AuroraData:
|
||||
"""Get aurora forecast."""
|
||||
|
||||
def __init__(self, latitude, longitude, threshold):
|
||||
|
||||
@@ -122,7 +122,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."""
|
||||
|
||||
@@ -8,7 +8,7 @@ import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.components.bbb_gpio as bbb_gpio
|
||||
from homeassistant.components import bbb_gpio
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.const import (DEVICE_DEFAULT_NAME, CONF_NAME)
|
||||
|
||||
@@ -25,6 +25,9 @@ DEFAULT_PAYLOAD_OFF = 'OFF'
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=60)
|
||||
|
||||
CONF_COMMAND_TIMEOUT = 'command_timeout'
|
||||
DEFAULT_TIMEOUT = 15
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_COMMAND): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
@@ -32,6 +35,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string,
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
|
||||
vol.Optional(
|
||||
CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
|
||||
})
|
||||
|
||||
|
||||
@@ -43,9 +48,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
payload_on = config.get(CONF_PAYLOAD_ON)
|
||||
device_class = config.get(CONF_DEVICE_CLASS)
|
||||
value_template = config.get(CONF_VALUE_TEMPLATE)
|
||||
command_timeout = config.get(CONF_COMMAND_TIMEOUT)
|
||||
if value_template is not None:
|
||||
value_template.hass = hass
|
||||
data = CommandSensorData(hass, command)
|
||||
data = CommandSensorData(hass, command, command_timeout)
|
||||
|
||||
add_devices([CommandBinarySensor(
|
||||
hass, data, name, device_class, payload_on, payload_off,
|
||||
|
||||
@@ -117,7 +117,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
add_entities(entities)
|
||||
|
||||
|
||||
class HikvisionData(object):
|
||||
class HikvisionData:
|
||||
"""Hikvision device event stream object."""
|
||||
|
||||
def __init__(self, hass, url, port, name, username, password):
|
||||
|
||||
@@ -16,10 +16,7 @@ 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,
|
||||
@@ -30,15 +27,18 @@ async def async_setup_platform(hass, config, async_add_devices,
|
||||
|
||||
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)
|
||||
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)
|
||||
@@ -47,10 +47,6 @@ async def async_setup_entry(hass, config_entry, async_add_devices):
|
||||
class HomematicipShutterContact(HomematicipGenericDevice, BinarySensorDevice):
|
||||
"""HomematicIP shutter contact."""
|
||||
|
||||
def __init__(self, home, device):
|
||||
"""Initialize the shutter contact."""
|
||||
super().__init__(home, device)
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor."""
|
||||
@@ -69,11 +65,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)
|
||||
"""HomematicIP motion detector."""
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
@@ -86,3 +78,17 @@ class HomematicipMotionDetector(HomematicipGenericDevice, BinarySensorDevice):
|
||||
if self._device.sabotage:
|
||||
return True
|
||||
return self._device.motionDetected
|
||||
|
||||
|
||||
class HomematicipSmokeDetector(HomematicipGenericDevice, BinarySensorDevice):
|
||||
"""HomematicIP 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
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.ihc/
|
||||
"""
|
||||
from xml.etree.ElementTree import Element
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
@@ -70,7 +68,7 @@ class IHCBinarySensor(IHCDevice, BinarySensorDevice):
|
||||
|
||||
def __init__(self, ihc_controller, name, ihc_id: int, info: bool,
|
||||
sensor_type: str, inverting: bool,
|
||||
product: Element = None) -> None:
|
||||
product=None) -> None:
|
||||
"""Initialize the IHC binary sensor."""
|
||||
super().__init__(ihc_controller, name, ihc_id, info, product)
|
||||
self._state = None
|
||||
|
||||
@@ -17,7 +17,9 @@ _LOGGER = logging.getLogger(__name__)
|
||||
SENSOR_TYPES = {'openClosedSensor': 'opening',
|
||||
'motionSensor': 'motion',
|
||||
'doorSensor': 'door',
|
||||
'wetLeakSensor': 'moisture'}
|
||||
'wetLeakSensor': 'moisture',
|
||||
'lightSensor': 'light',
|
||||
'batterySensor': 'battery'}
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
@@ -54,4 +56,9 @@ class InsteonPLMBinarySensor(InsteonPLMEntity, BinarySensorDevice):
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return the boolean response if the node is on."""
|
||||
return bool(self._insteon_device_state.value)
|
||||
on_val = bool(self._insteon_device_state.value)
|
||||
|
||||
if self._insteon_device_state.name == 'lightSensor':
|
||||
return not on_val
|
||||
|
||||
return on_val
|
||||
|
||||
@@ -101,7 +101,7 @@ class IssBinarySensor(BinarySensorDevice):
|
||||
self.iss_data.update()
|
||||
|
||||
|
||||
class IssData(object):
|
||||
class IssData:
|
||||
"""Get data from the ISS API."""
|
||||
|
||||
def __init__(self, latitude, longitude):
|
||||
|
||||
@@ -55,7 +55,7 @@ def setup_platform(hass, config: ConfigType,
|
||||
else:
|
||||
device_type = _detect_device_type(node)
|
||||
subnode_id = int(node.nid[-1])
|
||||
if (device_type == 'opening' or device_type == 'moisture'):
|
||||
if device_type in ('opening', 'moisture'):
|
||||
# These sensors use an optional "negative" subnode 2 to snag
|
||||
# all state changes
|
||||
if subnode_id == 2:
|
||||
|
||||
@@ -7,7 +7,7 @@ https://home-assistant.io/components/binary_sensor.modbus/
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.components.modbus as modbus
|
||||
from homeassistant.components import modbus
|
||||
from homeassistant.const import CONF_NAME, CONF_SLAVE
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
@@ -11,7 +11,7 @@ from typing import Optional
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
from homeassistant.components import mqtt
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, DEVICE_CLASSES_SCHEMA)
|
||||
from homeassistant.const import (
|
||||
|
||||
@@ -142,7 +142,7 @@ class NetatmoBinarySensor(BinarySensorDevice):
|
||||
"""Return the class of this sensor, from DEVICE_CLASSES."""
|
||||
if self._cameratype == 'NACamera':
|
||||
return WELCOME_SENSOR_TYPES.get(self._sensor_name)
|
||||
elif self._cameratype == 'NOC':
|
||||
if self._cameratype == 'NOC':
|
||||
return PRESENCE_SENSOR_TYPES.get(self._sensor_name)
|
||||
return TAG_SENSOR_TYPES.get(self._sensor_name)
|
||||
|
||||
|
||||
103
homeassistant/components/binary_sensor/openuv.py
Normal file
103
homeassistant/components/binary_sensor/openuv.py
Normal file
@@ -0,0 +1,103 @@
|
||||
"""
|
||||
This platform provides binary sensors for OpenUV data.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.openuv/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.const import CONF_MONITORED_CONDITIONS
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.components.openuv import (
|
||||
BINARY_SENSORS, DATA_PROTECTION_WINDOW, DOMAIN, TOPIC_UPDATE,
|
||||
TYPE_PROTECTION_WINDOW, OpenUvEntity)
|
||||
from homeassistant.util.dt import as_local, parse_datetime, utcnow
|
||||
|
||||
DEPENDENCIES = ['openuv']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_PROTECTION_WINDOW_STARTING_TIME = 'start_time'
|
||||
ATTR_PROTECTION_WINDOW_STARTING_UV = 'start_uv'
|
||||
ATTR_PROTECTION_WINDOW_ENDING_TIME = 'end_time'
|
||||
ATTR_PROTECTION_WINDOW_ENDING_UV = 'end_uv'
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up the OpenUV binary sensor platform."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
|
||||
openuv = hass.data[DOMAIN]
|
||||
|
||||
binary_sensors = []
|
||||
for sensor_type in discovery_info[CONF_MONITORED_CONDITIONS]:
|
||||
name, icon = BINARY_SENSORS[sensor_type]
|
||||
binary_sensors.append(
|
||||
OpenUvBinarySensor(openuv, sensor_type, name, icon))
|
||||
|
||||
async_add_devices(binary_sensors, True)
|
||||
|
||||
|
||||
class OpenUvBinarySensor(OpenUvEntity, BinarySensorDevice):
|
||||
"""Define a binary sensor for OpenUV."""
|
||||
|
||||
def __init__(self, openuv, sensor_type, name, icon):
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(openuv)
|
||||
|
||||
self._icon = icon
|
||||
self._latitude = openuv.client.latitude
|
||||
self._longitude = openuv.client.longitude
|
||||
self._name = name
|
||||
self._sensor_type = sensor_type
|
||||
self._state = None
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon."""
|
||||
return self._icon
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return the status of the sensor."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Disable polling."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return a unique, HASS-friendly identifier for this entity."""
|
||||
return '{0}_{1}_{2}'.format(
|
||||
self._latitude, self._longitude, self._sensor_type)
|
||||
|
||||
@callback
|
||||
def _update_data(self):
|
||||
"""Update the state."""
|
||||
self.async_schedule_update_ha_state(True)
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
async_dispatcher_connect(
|
||||
self.hass, TOPIC_UPDATE, self._update_data)
|
||||
|
||||
async def async_update(self):
|
||||
"""Update the state."""
|
||||
data = self.openuv.data[DATA_PROTECTION_WINDOW]['result']
|
||||
if self._sensor_type == TYPE_PROTECTION_WINDOW:
|
||||
self._state = parse_datetime(
|
||||
data['from_time']) <= utcnow() <= parse_datetime(
|
||||
data['to_time'])
|
||||
self._attrs.update({
|
||||
ATTR_PROTECTION_WINDOW_ENDING_TIME:
|
||||
as_local(parse_datetime(data['to_time'])),
|
||||
ATTR_PROTECTION_WINDOW_ENDING_UV: data['to_uv'],
|
||||
ATTR_PROTECTION_WINDOW_STARTING_UV: data['from_uv'],
|
||||
ATTR_PROTECTION_WINDOW_STARTING_TIME:
|
||||
as_local(parse_datetime(data['from_time'])),
|
||||
})
|
||||
@@ -96,7 +96,7 @@ class PingBinarySensor(BinarySensorDevice):
|
||||
self.ping.update()
|
||||
|
||||
|
||||
class PingData(object):
|
||||
class PingData:
|
||||
"""The Class for handling the data retrieval."""
|
||||
|
||||
def __init__(self, host, count):
|
||||
|
||||
@@ -111,11 +111,10 @@ class RachioControllerOnlineBinarySensor(RachioControllerBinarySensor):
|
||||
|
||||
if data[KEY_STATUS] == STATUS_ONLINE:
|
||||
return True
|
||||
elif data[KEY_STATUS] == STATUS_OFFLINE:
|
||||
if data[KEY_STATUS] == STATUS_OFFLINE:
|
||||
return False
|
||||
else:
|
||||
_LOGGER.warning('"%s" reported in unknown state "%s"', self.name,
|
||||
data[KEY_STATUS])
|
||||
_LOGGER.warning('"%s" reported in unknown state "%s"', self.name,
|
||||
data[KEY_STATUS])
|
||||
|
||||
def _handle_update(self, *args, **kwargs) -> None:
|
||||
"""Handle an update to the state of this sensor."""
|
||||
|
||||
@@ -67,6 +67,6 @@ class RainCloudBinarySensor(RainCloudEntity, BinarySensorDevice):
|
||||
"""Return the icon of this device."""
|
||||
if self._sensor_type == 'is_watering':
|
||||
return 'mdi:water' if self.is_on else 'mdi:water-off'
|
||||
elif self._sensor_type == 'status':
|
||||
if self._sensor_type == 'status':
|
||||
return 'mdi:pipe' if self.is_on else 'mdi:pipe-disconnected'
|
||||
return ICON_MAP.get(self._sensor_type)
|
||||
|
||||
@@ -8,7 +8,7 @@ import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.components.rpi_gpio as rpi_gpio
|
||||
from homeassistant.components import rpi_gpio
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.const import DEVICE_DEFAULT_NAME
|
||||
|
||||
@@ -10,7 +10,7 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
PLATFORM_SCHEMA, BinarySensorDevice)
|
||||
import homeassistant.components.rpi_pfio as rpi_pfio
|
||||
from homeassistant.components import rpi_pfio
|
||||
from homeassistant.const import CONF_NAME, DEVICE_DEFAULT_NAME
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
|
||||
98
homeassistant/components/binary_sensor/tahoma.py
Normal file
98
homeassistant/components/binary_sensor/tahoma.py
Normal file
@@ -0,0 +1,98 @@
|
||||
"""
|
||||
Support for Tahoma binary sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.tahoma/
|
||||
"""
|
||||
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice)
|
||||
from homeassistant.components.tahoma import (
|
||||
DOMAIN as TAHOMA_DOMAIN, TahomaDevice)
|
||||
from homeassistant.const import (STATE_OFF, STATE_ON, ATTR_BATTERY_LEVEL)
|
||||
|
||||
DEPENDENCIES = ['tahoma']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=120)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up Tahoma controller devices."""
|
||||
_LOGGER.debug("Setup Tahoma Binary sensor platform")
|
||||
controller = hass.data[TAHOMA_DOMAIN]['controller']
|
||||
devices = []
|
||||
for device in hass.data[TAHOMA_DOMAIN]['devices']['smoke']:
|
||||
devices.append(TahomaBinarySensor(device, controller))
|
||||
add_devices(devices, True)
|
||||
|
||||
|
||||
class TahomaBinarySensor(TahomaDevice, BinarySensorDevice):
|
||||
"""Representation of a Tahoma Binary Sensor."""
|
||||
|
||||
def __init__(self, tahoma_device, controller):
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(tahoma_device, controller)
|
||||
|
||||
self._state = None
|
||||
self._icon = None
|
||||
self._battery = None
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return the state of the sensor."""
|
||||
return bool(self._state == STATE_ON)
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of the device."""
|
||||
if self.tahoma_device.type == 'rtds:RTDSSmokeSensor':
|
||||
return 'smoke'
|
||||
return None
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Icon for device by its type."""
|
||||
return self._icon
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the device state attributes."""
|
||||
attr = {}
|
||||
super_attr = super().device_state_attributes
|
||||
if super_attr is not None:
|
||||
attr.update(super_attr)
|
||||
|
||||
if self._battery is not None:
|
||||
attr[ATTR_BATTERY_LEVEL] = self._battery
|
||||
return attr
|
||||
|
||||
def update(self):
|
||||
"""Update the state."""
|
||||
self.controller.get_states([self.tahoma_device])
|
||||
if self.tahoma_device.type == 'rtds:RTDSSmokeSensor':
|
||||
if self.tahoma_device.active_states['core:SmokeState']\
|
||||
== 'notDetected':
|
||||
self._state = STATE_OFF
|
||||
else:
|
||||
self._state = STATE_ON
|
||||
|
||||
if 'core:SensorDefectState' in self.tahoma_device.active_states:
|
||||
# Set to 'lowBattery' for low battery warning.
|
||||
self._battery = self.tahoma_device.active_states[
|
||||
'core:SensorDefectState']
|
||||
else:
|
||||
self._battery = None
|
||||
|
||||
if self._state == STATE_ON:
|
||||
self._icon = "mdi:fire"
|
||||
elif self._battery == 'lowBattery':
|
||||
self._icon = "mdi:battery-alert"
|
||||
else:
|
||||
self._icon = None
|
||||
|
||||
_LOGGER.debug("Update %s, state: %s", self._name, self._state)
|
||||
@@ -63,7 +63,7 @@ class TapsAffSensor(BinarySensorDevice):
|
||||
self.data.update()
|
||||
|
||||
|
||||
class TapsAffData(object):
|
||||
class TapsAffData:
|
||||
"""Class for handling the data retrieval for pins."""
|
||||
|
||||
def __init__(self, location):
|
||||
|
||||
@@ -86,7 +86,6 @@ class ThresholdSensor(BinarySensorDevice):
|
||||
self._state = False
|
||||
self.sensor_value = None
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
@callback
|
||||
def async_threshold_sensor_state_listener(
|
||||
entity, old_state, new_state):
|
||||
@@ -129,9 +128,9 @@ class ThresholdSensor(BinarySensorDevice):
|
||||
if self._threshold_lower is not None and \
|
||||
self._threshold_upper is not None:
|
||||
return TYPE_RANGE
|
||||
elif self._threshold_lower is not None:
|
||||
if self._threshold_lower is not None:
|
||||
return TYPE_LOWER
|
||||
elif self._threshold_upper is not None:
|
||||
if self._threshold_upper is not None:
|
||||
return TYPE_UPPER
|
||||
|
||||
@property
|
||||
|
||||
@@ -23,7 +23,7 @@ from homeassistant.helpers.entity import generate_entity_id
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
from homeassistant.util import utcnow
|
||||
|
||||
REQUIREMENTS = ['numpy==1.14.5']
|
||||
REQUIREMENTS = ['numpy==1.15.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -4,93 +4,34 @@ Support for Velbus Binary Sensors.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.velbus/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_NAME, CONF_DEVICES
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.binary_sensor import PLATFORM_SCHEMA
|
||||
from homeassistant.components.velbus import DOMAIN
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
|
||||
DEPENDENCIES = ['velbus']
|
||||
from homeassistant.components.velbus import (
|
||||
DOMAIN as VELBUS_DOMAIN, VelbusEntity)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_DEVICES): vol.All(cv.ensure_list, [
|
||||
{
|
||||
vol.Required('module'): cv.positive_int,
|
||||
vol.Required('channel'): cv.positive_int,
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Optional('is_pushbutton'): cv.boolean
|
||||
}
|
||||
])
|
||||
})
|
||||
DEPENDENCIES = ['velbus']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
async def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
"""Set up Velbus binary sensors."""
|
||||
velbus = hass.data[DOMAIN]
|
||||
|
||||
add_devices(VelbusBinarySensor(sensor, velbus)
|
||||
for sensor in config[CONF_DEVICES])
|
||||
if discovery_info is None:
|
||||
return
|
||||
sensors = []
|
||||
for sensor in discovery_info:
|
||||
module = hass.data[VELBUS_DOMAIN].get_module(sensor[0])
|
||||
channel = sensor[1]
|
||||
sensors.append(VelbusBinarySensor(module, channel))
|
||||
async_add_devices(sensors)
|
||||
|
||||
|
||||
class VelbusBinarySensor(BinarySensorDevice):
|
||||
class VelbusBinarySensor(VelbusEntity, BinarySensorDevice):
|
||||
"""Representation of a Velbus Binary Sensor."""
|
||||
|
||||
def __init__(self, binary_sensor, velbus):
|
||||
"""Initialize a Velbus light."""
|
||||
self._velbus = velbus
|
||||
self._name = binary_sensor[CONF_NAME]
|
||||
self._module = binary_sensor['module']
|
||||
self._channel = binary_sensor['channel']
|
||||
self._is_pushbutton = 'is_pushbutton' in binary_sensor \
|
||||
and binary_sensor['is_pushbutton']
|
||||
self._state = False
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Add listener for Velbus messages on bus."""
|
||||
yield from self.hass.async_add_job(
|
||||
self._velbus.subscribe, self._on_message)
|
||||
|
||||
def _on_message(self, message):
|
||||
import velbus
|
||||
if isinstance(message, velbus.PushButtonStatusMessage):
|
||||
if message.address == self._module and \
|
||||
self._channel in message.get_channels():
|
||||
if self._is_pushbutton:
|
||||
if self._channel in message.closed:
|
||||
self._toggle()
|
||||
else:
|
||||
pass
|
||||
else:
|
||||
self._toggle()
|
||||
|
||||
def _toggle(self):
|
||||
if self._state is True:
|
||||
self._state = False
|
||||
else:
|
||||
self._state = True
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the display name of this sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the sensor is on."""
|
||||
return self._state
|
||||
return self._module.is_closed(self._channel)
|
||||
|
||||
@@ -28,7 +28,7 @@ class VolvoSensor(VolvoEntity, BinarySensorDevice):
|
||||
val = getattr(self.vehicle, self._attribute)
|
||||
if self._attribute == 'bulb_failures':
|
||||
return bool(val)
|
||||
elif self._attribute in ['doors', 'windows']:
|
||||
if self._attribute in ['doors', 'windows']:
|
||||
return any([val[key] for key in val if 'Open' in key])
|
||||
return val != 'Normal'
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
"""Register discovered WeMo binary sensors."""
|
||||
import pywemo.discovery as discovery
|
||||
from pywemo import discovery
|
||||
|
||||
if discovery_info is not None:
|
||||
location = discovery_info['ssdp_description']
|
||||
|
||||
@@ -17,7 +17,7 @@ import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['holidays==0.9.5']
|
||||
REQUIREMENTS = ['holidays==0.9.6']
|
||||
|
||||
# List of all countries currently supported by holidays
|
||||
# There seems to be no way to get the list out at runtime
|
||||
@@ -25,9 +25,9 @@ ALL_COUNTRIES = ['Argentina', 'AR', 'Australia', 'AU', 'Austria', 'AT',
|
||||
'Belgium', 'BE', 'Canada', 'CA', 'Colombia', 'CO', 'Czech',
|
||||
'CZ', 'Denmark', 'DK', 'England', 'EuropeanCentralBank',
|
||||
'ECB', 'TAR', 'Finland', 'FI', 'France', 'FRA', 'Germany',
|
||||
'DE', 'Hungary', 'HU', 'Ireland', 'Isle of Man', 'Italy',
|
||||
'IT', 'Japan', 'JP', 'Mexico', 'MX', 'Netherlands', 'NL',
|
||||
'NewZealand', 'NZ', 'Northern Ireland',
|
||||
'DE', 'Hungary', 'HU', 'India', 'IND', 'Ireland',
|
||||
'Isle of Man', 'Italy', 'IT', 'Japan', 'JP', 'Mexico', 'MX',
|
||||
'Netherlands', 'NL', 'NewZealand', 'NZ', 'Northern Ireland',
|
||||
'Norway', 'NO', 'Polish', 'PL', 'Portugal', 'PT',
|
||||
'PortugalExt', 'PTE', 'Scotland', 'Slovenia', 'SI',
|
||||
'Slovakia', 'SK', 'South Africa', 'ZA', 'Spain', 'ES',
|
||||
@@ -135,7 +135,7 @@ class IsWorkdaySensor(BinarySensorDevice):
|
||||
"""Check if given day is in the includes list."""
|
||||
if day in self._workdays:
|
||||
return True
|
||||
elif 'holiday' in self._workdays and now in self._obj_holidays:
|
||||
if 'holiday' in self._workdays and now in self._obj_holidays:
|
||||
return True
|
||||
|
||||
return False
|
||||
@@ -144,7 +144,7 @@ class IsWorkdaySensor(BinarySensorDevice):
|
||||
"""Check if given day is in the excludes list."""
|
||||
if day in self._excludes:
|
||||
return True
|
||||
elif 'holiday' in self._excludes and now in self._obj_holidays:
|
||||
if 'holiday' in self._excludes and now in self._obj_holidays:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@@ -124,7 +124,7 @@ class XiaomiNatgasSensor(XiaomiBinarySensor):
|
||||
return False
|
||||
self._state = True
|
||||
return True
|
||||
elif value == '0':
|
||||
if value == '0':
|
||||
if self._state:
|
||||
self._state = False
|
||||
return True
|
||||
@@ -184,7 +184,7 @@ class XiaomiMotionSensor(XiaomiBinarySensor):
|
||||
return False
|
||||
self._state = True
|
||||
return True
|
||||
elif value == NO_MOTION:
|
||||
if value == NO_MOTION:
|
||||
if not self._state:
|
||||
return False
|
||||
self._state = False
|
||||
@@ -224,7 +224,7 @@ class XiaomiDoorSensor(XiaomiBinarySensor):
|
||||
return False
|
||||
self._state = True
|
||||
return True
|
||||
elif value == 'close':
|
||||
if value == 'close':
|
||||
self._open_since = 0
|
||||
if self._state:
|
||||
self._state = False
|
||||
@@ -254,7 +254,7 @@ class XiaomiWaterLeakSensor(XiaomiBinarySensor):
|
||||
return False
|
||||
self._state = True
|
||||
return True
|
||||
elif value == 'no_leak':
|
||||
if value == 'no_leak':
|
||||
if self._state:
|
||||
self._state = False
|
||||
return True
|
||||
@@ -290,7 +290,7 @@ class XiaomiSmokeSensor(XiaomiBinarySensor):
|
||||
return False
|
||||
self._state = True
|
||||
return True
|
||||
elif value == '0':
|
||||
if value == '0':
|
||||
if self._state:
|
||||
self._state = False
|
||||
return True
|
||||
|
||||
@@ -10,7 +10,7 @@ import homeassistant.util.dt as dt_util
|
||||
from homeassistant.helpers.event import track_point_in_time
|
||||
from homeassistant.components import zwave
|
||||
from homeassistant.components.zwave import workaround
|
||||
from homeassistant.components.zwave import async_setup_platform # noqa # pylint: disable=unused-import
|
||||
from homeassistant.components.zwave import async_setup_platform # noqa pylint: disable=unused-import
|
||||
from homeassistant.components.binary_sensor import (
|
||||
DOMAIN,
|
||||
BinarySensorDevice)
|
||||
|
||||
@@ -40,7 +40,7 @@ SNAP_PICTURE_SCHEMA = vol.Schema({
|
||||
})
|
||||
|
||||
|
||||
class BlinkSystem(object):
|
||||
class BlinkSystem:
|
||||
"""Blink System class."""
|
||||
|
||||
def __init__(self, config_info):
|
||||
|
||||
@@ -50,7 +50,7 @@ def setup(hass, config):
|
||||
return True
|
||||
|
||||
|
||||
class BloomSky(object):
|
||||
class BloomSky:
|
||||
"""Handle all communication with the BloomSky API."""
|
||||
|
||||
# API documentation at http://weatherlution.com/bloomsky-api/
|
||||
|
||||
@@ -118,7 +118,7 @@ def setup_account(account_config: dict, hass, name: str) \
|
||||
return cd_account
|
||||
|
||||
|
||||
class BMWConnectedDriveAccount(object):
|
||||
class BMWConnectedDriveAccount:
|
||||
"""Representation of a BMW vehicle."""
|
||||
|
||||
def __init__(self, username: str, password: str, region_str: str,
|
||||
|
||||
@@ -130,7 +130,7 @@ class CalendarEventDevice(Entity):
|
||||
|
||||
now = dt.now()
|
||||
|
||||
if start <= now and end > now:
|
||||
if start <= now < end:
|
||||
return STATE_ON
|
||||
|
||||
if now >= end:
|
||||
|
||||
@@ -125,7 +125,7 @@ class WebDavCalendarEventDevice(CalendarEventDevice):
|
||||
return await self.data.async_get_events(hass, start_date, end_date)
|
||||
|
||||
|
||||
class WebDavCalendarData(object):
|
||||
class WebDavCalendarData:
|
||||
"""Class to utilize the calendar dav client object to get next event."""
|
||||
|
||||
def __init__(self, calendar, include_all_day, search):
|
||||
|
||||
@@ -28,7 +28,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
])
|
||||
|
||||
|
||||
class DemoGoogleCalendarData(object):
|
||||
class DemoGoogleCalendarData:
|
||||
"""Representation of a Demo Calendar element."""
|
||||
|
||||
event = {}
|
||||
|
||||
@@ -55,7 +55,7 @@ class GoogleCalendarEventDevice(CalendarEventDevice):
|
||||
return await self.data.async_get_events(hass, start_date, end_date)
|
||||
|
||||
|
||||
class GoogleCalendarData(object):
|
||||
class GoogleCalendarData:
|
||||
"""Class to utilize calendar service object to get next event."""
|
||||
|
||||
def __init__(self, calendar_service, calendar_id, search,
|
||||
|
||||
@@ -280,7 +280,7 @@ class TodoistProjectDevice(CalendarEventDevice):
|
||||
return attributes
|
||||
|
||||
|
||||
class TodoistProjectData(object):
|
||||
class TodoistProjectData:
|
||||
"""
|
||||
Class used by the Task Device service object to hold all Todoist Tasks.
|
||||
|
||||
@@ -503,7 +503,7 @@ class TodoistProjectData(object):
|
||||
time_format = '%a %d %b %Y %H:%M:%S %z'
|
||||
for task in project_task_data:
|
||||
due_date = datetime.strptime(task['due_date_utc'], time_format)
|
||||
if due_date > start_date and due_date < end_date:
|
||||
if start_date < due_date < end_date:
|
||||
event = {
|
||||
'uid': task['id'],
|
||||
'title': task['content'],
|
||||
|
||||
@@ -19,7 +19,8 @@ import async_timeout
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, \
|
||||
SERVICE_TURN_ON
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.helpers.entity import Entity
|
||||
@@ -47,6 +48,9 @@ STATE_RECORDING = 'recording'
|
||||
STATE_STREAMING = 'streaming'
|
||||
STATE_IDLE = 'idle'
|
||||
|
||||
# Bitfield of features supported by the camera entity
|
||||
SUPPORT_ON_OFF = 1
|
||||
|
||||
DEFAULT_CONTENT_TYPE = 'image/jpeg'
|
||||
ENTITY_IMAGE_URL = '/api/camera_proxy/{0}?token={1}'
|
||||
|
||||
@@ -79,6 +83,35 @@ class Image:
|
||||
content = attr.ib(type=bytes)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def turn_off(hass, entity_id=None):
|
||||
"""Turn off camera."""
|
||||
hass.add_job(async_turn_off, hass, entity_id)
|
||||
|
||||
|
||||
@bind_hass
|
||||
async def async_turn_off(hass, entity_id=None):
|
||||
"""Turn off camera."""
|
||||
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
|
||||
await hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, data)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def turn_on(hass, entity_id=None):
|
||||
"""Turn on camera."""
|
||||
hass.add_job(async_turn_on, hass, entity_id)
|
||||
|
||||
|
||||
@bind_hass
|
||||
async def async_turn_on(hass, entity_id=None):
|
||||
"""Turn on camera, and set operation mode."""
|
||||
data = {}
|
||||
if entity_id is not None:
|
||||
data[ATTR_ENTITY_ID] = entity_id
|
||||
|
||||
await hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def enable_motion_detection(hass, entity_id=None):
|
||||
"""Enable Motion Detection."""
|
||||
@@ -119,6 +152,9 @@ async def async_get_image(hass, entity_id, timeout=10):
|
||||
if camera is None:
|
||||
raise HomeAssistantError('Camera not found')
|
||||
|
||||
if not camera.is_on:
|
||||
raise HomeAssistantError('Camera is off')
|
||||
|
||||
with suppress(asyncio.CancelledError, asyncio.TimeoutError):
|
||||
with async_timeout.timeout(timeout, loop=hass.loop):
|
||||
image = await camera.async_camera_image()
|
||||
@@ -163,6 +199,12 @@ async def async_setup(hass, config):
|
||||
await camera.async_enable_motion_detection()
|
||||
elif service.service == SERVICE_DISABLE_MOTION:
|
||||
await camera.async_disable_motion_detection()
|
||||
elif service.service == SERVICE_TURN_OFF and \
|
||||
camera.supported_features & SUPPORT_ON_OFF:
|
||||
await camera.async_turn_off()
|
||||
elif service.service == SERVICE_TURN_ON and \
|
||||
camera.supported_features & SUPPORT_ON_OFF:
|
||||
await camera.async_turn_on()
|
||||
|
||||
if not camera.should_poll:
|
||||
continue
|
||||
@@ -200,6 +242,12 @@ async def async_setup(hass, config):
|
||||
except OSError as err:
|
||||
_LOGGER.error("Can't write image to file: %s", err)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_TURN_OFF, async_handle_camera_service,
|
||||
schema=CAMERA_SERVICE_SCHEMA)
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_TURN_ON, async_handle_camera_service,
|
||||
schema=CAMERA_SERVICE_SCHEMA)
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_ENABLE_MOTION, async_handle_camera_service,
|
||||
schema=CAMERA_SERVICE_SCHEMA)
|
||||
@@ -243,6 +291,11 @@ class Camera(Entity):
|
||||
"""Return a link to the camera feed as entity picture."""
|
||||
return ENTITY_IMAGE_URL.format(self.entity_id, self.access_tokens[-1])
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
return 0
|
||||
|
||||
@property
|
||||
def is_recording(self):
|
||||
"""Return true if the device is recording."""
|
||||
@@ -301,32 +354,23 @@ class Camera(Entity):
|
||||
|
||||
last_image = None
|
||||
|
||||
try:
|
||||
while True:
|
||||
img_bytes = await self.async_camera_image()
|
||||
if not img_bytes:
|
||||
break
|
||||
while True:
|
||||
img_bytes = await self.async_camera_image()
|
||||
if not img_bytes:
|
||||
break
|
||||
|
||||
if img_bytes and img_bytes != last_image:
|
||||
if img_bytes and img_bytes != last_image:
|
||||
await write_to_mjpeg_stream(img_bytes)
|
||||
|
||||
# Chrome seems to always ignore first picture,
|
||||
# print it twice.
|
||||
if last_image is None:
|
||||
await write_to_mjpeg_stream(img_bytes)
|
||||
last_image = img_bytes
|
||||
|
||||
# Chrome seems to always ignore first picture,
|
||||
# print it twice.
|
||||
if last_image is None:
|
||||
await write_to_mjpeg_stream(img_bytes)
|
||||
await asyncio.sleep(interval)
|
||||
|
||||
last_image = img_bytes
|
||||
|
||||
await asyncio.sleep(interval)
|
||||
|
||||
except asyncio.CancelledError:
|
||||
_LOGGER.debug("Stream closed by frontend.")
|
||||
response = None
|
||||
raise
|
||||
|
||||
finally:
|
||||
if response is not None:
|
||||
await response.write_eof()
|
||||
return response
|
||||
|
||||
async def handle_async_mjpeg_stream(self, request):
|
||||
"""Serve an HTTP MJPEG stream from the camera.
|
||||
@@ -342,14 +386,38 @@ class Camera(Entity):
|
||||
"""Return the camera state."""
|
||||
if self.is_recording:
|
||||
return STATE_RECORDING
|
||||
elif self.is_streaming:
|
||||
if self.is_streaming:
|
||||
return STATE_STREAMING
|
||||
return STATE_IDLE
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if on."""
|
||||
return True
|
||||
|
||||
def turn_off(self):
|
||||
"""Turn off camera."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@callback
|
||||
def async_turn_off(self):
|
||||
"""Turn off camera."""
|
||||
return self.hass.async_add_job(self.turn_off)
|
||||
|
||||
def turn_on(self):
|
||||
"""Turn off camera."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@callback
|
||||
def async_turn_on(self):
|
||||
"""Turn off camera."""
|
||||
return self.hass.async_add_job(self.turn_on)
|
||||
|
||||
def enable_motion_detection(self):
|
||||
"""Enable motion detection in the camera."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@callback
|
||||
def async_enable_motion_detection(self):
|
||||
"""Call the job and enable motion detection."""
|
||||
return self.hass.async_add_job(self.enable_motion_detection)
|
||||
@@ -358,6 +426,7 @@ class Camera(Entity):
|
||||
"""Disable motion detection in camera."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@callback
|
||||
def async_disable_motion_detection(self):
|
||||
"""Call the job and disable motion detection."""
|
||||
return self.hass.async_add_job(self.disable_motion_detection)
|
||||
@@ -402,17 +471,19 @@ class CameraView(HomeAssistantView):
|
||||
camera = self.component.get_entity(entity_id)
|
||||
|
||||
if camera is None:
|
||||
status = 404 if request[KEY_AUTHENTICATED] else 401
|
||||
return web.Response(status=status)
|
||||
raise web.HTTPNotFound()
|
||||
|
||||
authenticated = (request[KEY_AUTHENTICATED] or
|
||||
request.query.get('token') in camera.access_tokens)
|
||||
|
||||
if not authenticated:
|
||||
return web.Response(status=401)
|
||||
raise web.HTTPUnauthorized()
|
||||
|
||||
response = await self.handle(request, camera)
|
||||
return response
|
||||
if not camera.is_on:
|
||||
_LOGGER.debug('Camera is off.')
|
||||
raise web.HTTPServiceUnavailable()
|
||||
|
||||
return await self.handle(request, camera)
|
||||
|
||||
async def handle(self, request, camera):
|
||||
"""Handle the camera request."""
|
||||
@@ -435,7 +506,7 @@ class CameraImageView(CameraView):
|
||||
return web.Response(body=image,
|
||||
content_type=camera.content_type)
|
||||
|
||||
return web.Response(status=500)
|
||||
raise web.HTTPInternalServerError()
|
||||
|
||||
|
||||
class CameraMjpegStream(CameraView):
|
||||
@@ -448,8 +519,7 @@ class CameraMjpegStream(CameraView):
|
||||
"""Serve camera stream, possibly with interval."""
|
||||
interval = request.query.get('interval')
|
||||
if interval is None:
|
||||
await camera.handle_async_mjpeg_stream(request)
|
||||
return
|
||||
return await camera.handle_async_mjpeg_stream(request)
|
||||
|
||||
try:
|
||||
# Compose camera stream from stills
|
||||
@@ -457,10 +527,9 @@ class CameraMjpegStream(CameraView):
|
||||
if interval < MIN_STREAM_INTERVAL:
|
||||
raise ValueError("Stream interval must be be > {}"
|
||||
.format(MIN_STREAM_INTERVAL))
|
||||
await camera.handle_async_still_stream(request, interval)
|
||||
return
|
||||
return await camera.handle_async_still_stream(request, interval)
|
||||
except ValueError:
|
||||
return web.Response(status=400)
|
||||
raise web.HTTPBadRequest()
|
||||
|
||||
|
||||
@callback
|
||||
|
||||
@@ -64,7 +64,7 @@ class AmcrestCam(Camera):
|
||||
yield from super().handle_async_mjpeg_stream(request)
|
||||
return
|
||||
|
||||
elif self._stream_source == STREAM_SOURCE_LIST['mjpeg']:
|
||||
if self._stream_source == STREAM_SOURCE_LIST['mjpeg']:
|
||||
# stream an MJPEG image stream directly from the camera
|
||||
websession = async_get_clientsession(self.hass)
|
||||
streaming_url = self._camera.mjpeg_url(typeno=self._resolution)
|
||||
|
||||
@@ -23,7 +23,7 @@ def _get_image_url(host, port, mode):
|
||||
"""Set the URL to get the image."""
|
||||
if mode == 'mjpeg':
|
||||
return 'http://{}:{}/axis-cgi/mjpg/video.cgi'.format(host, port)
|
||||
elif mode == 'single':
|
||||
if mode == 'single':
|
||||
return 'http://{}:{}/axis-cgi/jpg/image.cgi'.format(host, port)
|
||||
|
||||
|
||||
|
||||
@@ -4,10 +4,10 @@ Demo camera platform that has a fake camera.
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/demo/
|
||||
"""
|
||||
import os
|
||||
import logging
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.components.camera import Camera
|
||||
import os
|
||||
|
||||
from homeassistant.components.camera import Camera, SUPPORT_ON_OFF
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -16,26 +16,29 @@ async def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
"""Set up the Demo camera platform."""
|
||||
async_add_devices([
|
||||
DemoCamera(hass, config, 'Demo camera')
|
||||
DemoCamera('Demo camera')
|
||||
])
|
||||
|
||||
|
||||
class DemoCamera(Camera):
|
||||
"""The representation of a Demo camera."""
|
||||
|
||||
def __init__(self, hass, config, name):
|
||||
def __init__(self, name):
|
||||
"""Initialize demo camera component."""
|
||||
super().__init__()
|
||||
self._parent = hass
|
||||
self._name = name
|
||||
self._motion_status = False
|
||||
self.is_streaming = True
|
||||
self._images_index = 0
|
||||
|
||||
def camera_image(self):
|
||||
"""Return a faked still image response."""
|
||||
now = dt_util.utcnow()
|
||||
self._images_index = (self._images_index + 1) % 4
|
||||
|
||||
image_path = os.path.join(
|
||||
os.path.dirname(__file__), 'demo_{}.jpg'.format(now.second % 4))
|
||||
os.path.dirname(__file__),
|
||||
'demo_{}.jpg'.format(self._images_index))
|
||||
_LOGGER.debug('Loading camera_image: %s', image_path)
|
||||
with open(image_path, 'rb') as file:
|
||||
return file.read()
|
||||
|
||||
@@ -46,8 +49,21 @@ class DemoCamera(Camera):
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Camera should poll periodically."""
|
||||
return True
|
||||
"""Demo camera doesn't need poll.
|
||||
|
||||
Need explicitly call schedule_update_ha_state() after state changed.
|
||||
"""
|
||||
return False
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Camera support turn on/off features."""
|
||||
return SUPPORT_ON_OFF
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Whether camera is on (streaming)."""
|
||||
return self.is_streaming
|
||||
|
||||
@property
|
||||
def motion_detection_enabled(self):
|
||||
@@ -57,7 +73,19 @@ class DemoCamera(Camera):
|
||||
def enable_motion_detection(self):
|
||||
"""Enable the Motion detection in base station (Arm)."""
|
||||
self._motion_status = True
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def disable_motion_detection(self):
|
||||
"""Disable the motion detection in base station (Disarm)."""
|
||||
self._motion_status = False
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def turn_off(self):
|
||||
"""Turn off camera."""
|
||||
self.is_streaming = False
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def turn_on(self):
|
||||
"""Turn on camera."""
|
||||
self.is_streaming = True
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@@ -29,8 +29,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
async def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
"""Set up a FFmpeg camera."""
|
||||
if not hass.data[DATA_FFMPEG].async_run_test(config.get(CONF_INPUT)):
|
||||
return
|
||||
@@ -49,30 +49,30 @@ class FFmpegCamera(Camera):
|
||||
self._input = config.get(CONF_INPUT)
|
||||
self._extra_arguments = config.get(CONF_EXTRA_ARGUMENTS)
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_camera_image(self):
|
||||
async def async_camera_image(self):
|
||||
"""Return a still image response from the camera."""
|
||||
from haffmpeg import ImageFrame, IMAGE_JPEG
|
||||
ffmpeg = ImageFrame(self._manager.binary, loop=self.hass.loop)
|
||||
|
||||
image = yield from asyncio.shield(ffmpeg.get_image(
|
||||
image = await asyncio.shield(ffmpeg.get_image(
|
||||
self._input, output_format=IMAGE_JPEG,
|
||||
extra_cmd=self._extra_arguments), loop=self.hass.loop)
|
||||
return image
|
||||
|
||||
@asyncio.coroutine
|
||||
def handle_async_mjpeg_stream(self, request):
|
||||
async def handle_async_mjpeg_stream(self, request):
|
||||
"""Generate an HTTP MJPEG stream from the camera."""
|
||||
from haffmpeg import CameraMjpeg
|
||||
|
||||
stream = CameraMjpeg(self._manager.binary, loop=self.hass.loop)
|
||||
yield from stream.open_camera(
|
||||
await stream.open_camera(
|
||||
self._input, extra_cmd=self._extra_arguments)
|
||||
|
||||
yield from async_aiohttp_proxy_stream(
|
||||
self.hass, request, stream,
|
||||
'multipart/x-mixed-replace;boundary=ffserver')
|
||||
yield from stream.close()
|
||||
try:
|
||||
return await async_aiohttp_proxy_stream(
|
||||
self.hass, request, stream,
|
||||
'multipart/x-mixed-replace;boundary=ffserver')
|
||||
finally:
|
||||
await stream.close()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
||||
@@ -123,19 +123,18 @@ class MjpegCamera(Camera):
|
||||
with closing(req) as response:
|
||||
return extract_image_from_mjpeg(response.iter_content(102400))
|
||||
|
||||
@asyncio.coroutine
|
||||
def handle_async_mjpeg_stream(self, request):
|
||||
async def handle_async_mjpeg_stream(self, request):
|
||||
"""Generate an HTTP MJPEG stream from the camera."""
|
||||
# aiohttp don't support DigestAuth -> Fallback
|
||||
if self._authentication == HTTP_DIGEST_AUTHENTICATION:
|
||||
yield from super().handle_async_mjpeg_stream(request)
|
||||
await super().handle_async_mjpeg_stream(request)
|
||||
return
|
||||
|
||||
# connect to stream
|
||||
websession = async_get_clientsession(self.hass)
|
||||
stream_coro = websession.get(self._mjpeg_url, auth=self._auth)
|
||||
|
||||
yield from async_aiohttp_proxy_web(self.hass, request, stream_coro)
|
||||
return await async_aiohttp_proxy_web(self.hass, request, stream_coro)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
||||
@@ -11,7 +11,7 @@ import logging
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
from homeassistant.components import mqtt
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
@@ -9,8 +9,9 @@ from datetime import timedelta
|
||||
|
||||
import requests
|
||||
|
||||
import homeassistant.components.nest as nest
|
||||
from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera)
|
||||
from homeassistant.components import nest
|
||||
from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera,
|
||||
SUPPORT_ON_OFF)
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -76,7 +77,36 @@ class NestCamera(Camera):
|
||||
"""Return the brand of the camera."""
|
||||
return NEST_BRAND
|
||||
|
||||
# This doesn't seem to be getting called regularly, for some reason
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Nest Cam support turn on and off."""
|
||||
return SUPPORT_ON_OFF
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if on."""
|
||||
return self._online and self._is_streaming
|
||||
|
||||
def turn_off(self):
|
||||
"""Turn off camera."""
|
||||
_LOGGER.debug('Turn off camera %s', self._name)
|
||||
# Calling Nest API in is_streaming setter.
|
||||
# device.is_streaming would not immediately change until the process
|
||||
# finished in Nest Cam.
|
||||
self.device.is_streaming = False
|
||||
|
||||
def turn_on(self):
|
||||
"""Turn on camera."""
|
||||
if not self._online:
|
||||
_LOGGER.error('Camera %s is offline.', self._name)
|
||||
return
|
||||
|
||||
_LOGGER.debug('Turn on camera %s', self._name)
|
||||
# Calling Nest API in is_streaming setter.
|
||||
# device.is_streaming would not immediately change until the process
|
||||
# finished in Nest Cam.
|
||||
self.device.is_streaming = True
|
||||
|
||||
def update(self):
|
||||
"""Cache value from Python-nest."""
|
||||
self._location = self.device.where
|
||||
|
||||
@@ -105,6 +105,6 @@ class NetatmoCamera(Camera):
|
||||
"""Return the camera model."""
|
||||
if self._cameratype == "NOC":
|
||||
return "Presence"
|
||||
elif self._cameratype == "NACamera":
|
||||
if self._cameratype == "NACamera":
|
||||
return "Welcome"
|
||||
return None
|
||||
|
||||
@@ -2,56 +2,53 @@
|
||||
Proxy camera platform that enables image processing of camera data.
|
||||
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/proxy
|
||||
https://www.home-assistant.io/components/camera.proxy/
|
||||
"""
|
||||
import logging
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import aiohttp
|
||||
import async_timeout
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.util.async_ import run_coroutine_threadsafe
|
||||
from homeassistant.components.camera import PLATFORM_SCHEMA, Camera
|
||||
from homeassistant.const import CONF_ENTITY_ID, CONF_NAME, HTTP_HEADER_HA_AUTH
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_ENTITY_ID, HTTP_HEADER_HA_AUTH)
|
||||
from homeassistant.components.camera import (
|
||||
PLATFORM_SCHEMA, Camera)
|
||||
from homeassistant.helpers.aiohttp_client import (
|
||||
async_get_clientsession, async_aiohttp_proxy_web)
|
||||
async_aiohttp_proxy_web, async_get_clientsession)
|
||||
from homeassistant.util.async_ import run_coroutine_threadsafe
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
REQUIREMENTS = ['pillow==5.0.0']
|
||||
REQUIREMENTS = ['pillow==5.2.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_MAX_IMAGE_WIDTH = "max_image_width"
|
||||
CONF_IMAGE_QUALITY = "image_quality"
|
||||
CONF_IMAGE_REFRESH_RATE = "image_refresh_rate"
|
||||
CONF_FORCE_RESIZE = "force_resize"
|
||||
CONF_MAX_STREAM_WIDTH = "max_stream_width"
|
||||
CONF_STREAM_QUALITY = "stream_quality"
|
||||
CONF_CACHE_IMAGES = "cache_images"
|
||||
CONF_CACHE_IMAGES = 'cache_images'
|
||||
CONF_FORCE_RESIZE = 'force_resize'
|
||||
CONF_IMAGE_QUALITY = 'image_quality'
|
||||
CONF_IMAGE_REFRESH_RATE = 'image_refresh_rate'
|
||||
CONF_MAX_IMAGE_WIDTH = 'max_image_width'
|
||||
CONF_MAX_STREAM_WIDTH = 'max_stream_width'
|
||||
CONF_STREAM_QUALITY = 'stream_quality'
|
||||
|
||||
DEFAULT_BASENAME = "Camera Proxy"
|
||||
DEFAULT_QUALITY = 75
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_ENTITY_ID): cv.entity_id,
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_MAX_IMAGE_WIDTH): int,
|
||||
vol.Optional(CONF_CACHE_IMAGES, False): cv.boolean,
|
||||
vol.Optional(CONF_FORCE_RESIZE, False): cv.boolean,
|
||||
vol.Optional(CONF_IMAGE_QUALITY): int,
|
||||
vol.Optional(CONF_IMAGE_REFRESH_RATE): float,
|
||||
vol.Optional(CONF_FORCE_RESIZE, False): cv.boolean,
|
||||
vol.Optional(CONF_CACHE_IMAGES, False): cv.boolean,
|
||||
vol.Optional(CONF_MAX_IMAGE_WIDTH): int,
|
||||
vol.Optional(CONF_MAX_STREAM_WIDTH): int,
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_STREAM_QUALITY): int,
|
||||
})
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_devices,
|
||||
discovery_info=None):
|
||||
async def async_setup_platform(
|
||||
hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up the Proxy camera platform."""
|
||||
async_add_devices([ProxyCamera(hass, config)])
|
||||
|
||||
@@ -69,7 +66,7 @@ def _resize_image(image, opts):
|
||||
|
||||
img = Image.open(io.BytesIO(image))
|
||||
imgfmt = str(img.format)
|
||||
if imgfmt != 'PNG' and imgfmt != 'JPEG':
|
||||
if imgfmt not in ('PNG', 'JPEG'):
|
||||
_LOGGER.debug("Image is of unsupported type: %s", imgfmt)
|
||||
return image
|
||||
|
||||
@@ -77,7 +74,7 @@ def _resize_image(image, opts):
|
||||
old_size = len(image)
|
||||
if old_width <= new_width:
|
||||
if opts.quality is None:
|
||||
_LOGGER.debug("Image is smaller-than / equal-to requested width")
|
||||
_LOGGER.debug("Image is smaller-than/equal-to requested width")
|
||||
return image
|
||||
new_width = old_width
|
||||
|
||||
@@ -86,7 +83,7 @@ def _resize_image(image, opts):
|
||||
|
||||
img = img.resize((new_width, new_height), Image.ANTIALIAS)
|
||||
imgbuf = io.BytesIO()
|
||||
img.save(imgbuf, "JPEG", optimize=True, quality=quality)
|
||||
img.save(imgbuf, 'JPEG', optimize=True, quality=quality)
|
||||
newimage = imgbuf.getvalue()
|
||||
if not opts.force_resize and len(newimage) >= old_size:
|
||||
_LOGGER.debug("Using original image(%d bytes) "
|
||||
@@ -94,11 +91,9 @@ def _resize_image(image, opts):
|
||||
old_size, len(newimage))
|
||||
return image
|
||||
|
||||
_LOGGER.debug("Resized image "
|
||||
"from (%dx%d - %d bytes) "
|
||||
"to (%dx%d - %d bytes)",
|
||||
old_width, old_height, old_size,
|
||||
new_width, new_height, len(newimage))
|
||||
_LOGGER.debug(
|
||||
"Resized image from (%dx%d - %d bytes) to (%dx%d - %d bytes)",
|
||||
old_width, old_height, old_size, new_width, new_height, len(newimage))
|
||||
return newimage
|
||||
|
||||
|
||||
@@ -112,7 +107,7 @@ class ImageOpts():
|
||||
self.force_resize = force_resize
|
||||
|
||||
def __bool__(self):
|
||||
"""Bool evalution rules."""
|
||||
"""Bool evaluation rules."""
|
||||
return bool(self.max_width or self.quality)
|
||||
|
||||
|
||||
@@ -133,8 +128,7 @@ class ProxyCamera(Camera):
|
||||
config.get(CONF_FORCE_RESIZE))
|
||||
|
||||
self._stream_opts = ImageOpts(
|
||||
config.get(CONF_MAX_STREAM_WIDTH),
|
||||
config.get(CONF_STREAM_QUALITY),
|
||||
config.get(CONF_MAX_STREAM_WIDTH), config.get(CONF_STREAM_QUALITY),
|
||||
True)
|
||||
|
||||
self._image_refresh_rate = config.get(CONF_IMAGE_REFRESH_RATE)
|
||||
@@ -145,8 +139,7 @@ class ProxyCamera(Camera):
|
||||
self._last_image = None
|
||||
self._headers = (
|
||||
{HTTP_HEADER_HA_AUTH: self.hass.config.api.api_password}
|
||||
if self.hass.config.api.api_password is not None
|
||||
else None)
|
||||
if self.hass.config.api.api_password is not None else None)
|
||||
|
||||
def camera_image(self):
|
||||
"""Return camera image."""
|
||||
@@ -191,12 +184,12 @@ class ProxyCamera(Camera):
|
||||
stream_coro = websession.get(url, headers=self._headers)
|
||||
|
||||
if not self._stream_opts:
|
||||
await async_aiohttp_proxy_web(self.hass, request, stream_coro)
|
||||
return
|
||||
return await async_aiohttp_proxy_web(
|
||||
self.hass, request, stream_coro)
|
||||
|
||||
response = aiohttp.web.StreamResponse()
|
||||
response.content_type = ('multipart/x-mixed-replace; '
|
||||
'boundary=--frameboundary')
|
||||
response.content_type = (
|
||||
'multipart/x-mixed-replace; boundary=--frameboundary')
|
||||
await response.prepare(request)
|
||||
|
||||
async def write(img_bytes):
|
||||
@@ -229,15 +222,10 @@ class ProxyCamera(Camera):
|
||||
_resize_image, image, self._stream_opts)
|
||||
await write(image)
|
||||
data = data[jpg_end + 2:]
|
||||
except asyncio.CancelledError:
|
||||
_LOGGER.debug("Stream closed by frontend.")
|
||||
req.close()
|
||||
response = None
|
||||
raise
|
||||
|
||||
finally:
|
||||
if response is not None:
|
||||
await response.write_eof()
|
||||
req.close()
|
||||
|
||||
return response
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# Describes the format for available camera services
|
||||
|
||||
turn_off:
|
||||
description: Turn off camera.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Entity id.
|
||||
example: 'camera.living_room'
|
||||
|
||||
turn_on:
|
||||
description: Turn on camera.
|
||||
fields:
|
||||
entity_id:
|
||||
description: Entity id.
|
||||
example: 'camera.living_room'
|
||||
|
||||
enable_motion_detection:
|
||||
description: Enable the motion detection in a camera.
|
||||
fields:
|
||||
|
||||
@@ -171,10 +171,9 @@ class UnifiVideoCamera(Camera):
|
||||
if retry:
|
||||
self._login()
|
||||
return _get_image(retry=False)
|
||||
else:
|
||||
_LOGGER.error(
|
||||
"Unable to log into camera, unable to get snapshot")
|
||||
raise
|
||||
_LOGGER.error(
|
||||
"Unable to log into camera, unable to get snapshot")
|
||||
raise
|
||||
|
||||
return _get_image()
|
||||
|
||||
|
||||
@@ -66,8 +66,7 @@ class VerisureSmartcam(Camera):
|
||||
if not image_ids:
|
||||
return
|
||||
new_image_id = image_ids[0]
|
||||
if (new_image_id == '-1' or
|
||||
self._image_id == new_image_id):
|
||||
if new_image_id in ('-1', self._image_id):
|
||||
_LOGGER.debug("The image is the same, or loading image_id")
|
||||
return
|
||||
_LOGGER.debug("Download new image %s", new_image_id)
|
||||
|
||||
@@ -57,6 +57,7 @@ class YiCamera(Camera):
|
||||
self._last_url = None
|
||||
self._manager = hass.data[DATA_FFMPEG]
|
||||
self._name = config[CONF_NAME]
|
||||
self._is_on = True
|
||||
self.host = config[CONF_HOST]
|
||||
self.port = config[CONF_PORT]
|
||||
self.path = config[CONF_PATH]
|
||||
@@ -68,6 +69,11 @@ class YiCamera(Camera):
|
||||
"""Camera brand."""
|
||||
return DEFAULT_BRAND
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Determine whether the camera is on."""
|
||||
return self._is_on
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of this camera."""
|
||||
@@ -81,7 +87,7 @@ class YiCamera(Camera):
|
||||
try:
|
||||
await ftp.connect(self.host)
|
||||
await ftp.login(self.user, self.passwd)
|
||||
except StatusCodeError as err:
|
||||
except (ConnectionRefusedError, StatusCodeError) as err:
|
||||
raise PlatformNotReady(err)
|
||||
|
||||
try:
|
||||
@@ -101,12 +107,13 @@ class YiCamera(Camera):
|
||||
return None
|
||||
|
||||
await ftp.quit()
|
||||
|
||||
self._is_on = True
|
||||
return 'ftp://{0}:{1}@{2}:{3}{4}/{5}/{6}'.format(
|
||||
self.user, self.passwd, self.host, self.port, self.path,
|
||||
latest_dir, videos[-1])
|
||||
except (ConnectionRefusedError, StatusCodeError) as err:
|
||||
_LOGGER.error('Error while fetching video: %s', err)
|
||||
self._is_on = False
|
||||
return None
|
||||
|
||||
async def async_camera_image(self):
|
||||
@@ -114,7 +121,7 @@ class YiCamera(Camera):
|
||||
from haffmpeg import ImageFrame, IMAGE_JPEG
|
||||
|
||||
url = await self._get_latest_video_url()
|
||||
if url != self._last_url:
|
||||
if url and url != self._last_url:
|
||||
ffmpeg = ImageFrame(self._manager.binary, loop=self.hass.loop)
|
||||
self._last_image = await asyncio.shield(
|
||||
ffmpeg.get_image(
|
||||
@@ -130,6 +137,9 @@ class YiCamera(Camera):
|
||||
"""Generate an HTTP MJPEG stream from the camera."""
|
||||
from haffmpeg import CameraMjpeg
|
||||
|
||||
if not self._is_on:
|
||||
return
|
||||
|
||||
stream = CameraMjpeg(self._manager.binary, loop=self.hass.loop)
|
||||
await stream.open_camera(
|
||||
self._last_url, extra_cmd=self._extra_arguments)
|
||||
|
||||
@@ -12,7 +12,7 @@ from homeassistant.const import CONF_NAME
|
||||
from homeassistant.components.camera.mjpeg import (
|
||||
CONF_MJPEG_URL, CONF_STILL_IMAGE_URL, MjpegCamera)
|
||||
|
||||
import homeassistant.components.zoneminder as zoneminder
|
||||
from homeassistant.components import zoneminder
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ def setup(hass, config):
|
||||
return True
|
||||
|
||||
|
||||
class CanaryData(object):
|
||||
class CanaryData:
|
||||
"""Get the latest data and update the states."""
|
||||
|
||||
def __init__(self, username, password, timeout):
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"no_devices_found": "Keine Google Cast Ger\u00e4te im Netzwerk gefunden."
|
||||
"no_devices_found": "Keine Google Cast Ger\u00e4te im Netzwerk gefunden.",
|
||||
"single_instance_allowed": "Nur eine einzige Konfiguration von Google Cast ist notwendig."
|
||||
},
|
||||
"step": {
|
||||
"confirm": {
|
||||
"description": "M\u00f6chten Sie Google Cast einrichten?",
|
||||
"title": ""
|
||||
"title": "Google Cast"
|
||||
}
|
||||
},
|
||||
"title": ""
|
||||
"title": "Google Cast"
|
||||
}
|
||||
}
|
||||
15
homeassistant/components/cast/.translations/es-419.json
Normal file
15
homeassistant/components/cast/.translations/es-419.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"no_devices_found": "No se encontraron dispositivos Google Cast en la red.",
|
||||
"single_instance_allowed": "S\u00f3lo es necesaria una \u00fanica configuraci\u00f3n de Google Cast."
|
||||
},
|
||||
"step": {
|
||||
"confirm": {
|
||||
"description": "\u00bfDesea configurar Google Cast?",
|
||||
"title": "Google Cast"
|
||||
}
|
||||
},
|
||||
"title": "Google Cast"
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user