Merge branch 'dev' into rc
This commit is contained in:
commit
be61e2e714
634 changed files with 5774 additions and 2336 deletions
10
.coveragerc
10
.coveragerc
|
@ -251,6 +251,9 @@ omit =
|
||||||
homeassistant/components/scsgate.py
|
homeassistant/components/scsgate.py
|
||||||
homeassistant/components/*/scsgate.py
|
homeassistant/components/*/scsgate.py
|
||||||
|
|
||||||
|
homeassistant/components/sisyphus.py
|
||||||
|
homeassistant/components/*/sisyphus.py
|
||||||
|
|
||||||
homeassistant/components/skybell.py
|
homeassistant/components/skybell.py
|
||||||
homeassistant/components/*/skybell.py
|
homeassistant/components/*/skybell.py
|
||||||
|
|
||||||
|
@ -346,6 +349,9 @@ omit =
|
||||||
homeassistant/components/tuya.py
|
homeassistant/components/tuya.py
|
||||||
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/alarmdotcom.py
|
||||||
homeassistant/components/alarm_control_panel/canary.py
|
homeassistant/components/alarm_control_panel/canary.py
|
||||||
homeassistant/components/alarm_control_panel/concord232.py
|
homeassistant/components/alarm_control_panel/concord232.py
|
||||||
|
@ -398,6 +404,8 @@ omit =
|
||||||
homeassistant/components/climate/touchline.py
|
homeassistant/components/climate/touchline.py
|
||||||
homeassistant/components/climate/venstar.py
|
homeassistant/components/climate/venstar.py
|
||||||
homeassistant/components/climate/zhong_hong.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/garadget.py
|
||||||
homeassistant/components/cover/gogogate2.py
|
homeassistant/components/cover/gogogate2.py
|
||||||
homeassistant/components/cover/homematic.py
|
homeassistant/components/cover/homematic.py
|
||||||
|
@ -461,6 +469,7 @@ omit =
|
||||||
homeassistant/components/light/decora_wifi.py
|
homeassistant/components/light/decora_wifi.py
|
||||||
homeassistant/components/light/decora.py
|
homeassistant/components/light/decora.py
|
||||||
homeassistant/components/light/flux_led.py
|
homeassistant/components/light/flux_led.py
|
||||||
|
homeassistant/components/light/futurenow.py
|
||||||
homeassistant/components/light/greenwave.py
|
homeassistant/components/light/greenwave.py
|
||||||
homeassistant/components/light/hue.py
|
homeassistant/components/light/hue.py
|
||||||
homeassistant/components/light/hyperion.py
|
homeassistant/components/light/hyperion.py
|
||||||
|
@ -657,6 +666,7 @@ omit =
|
||||||
homeassistant/components/sensor/loopenergy.py
|
homeassistant/components/sensor/loopenergy.py
|
||||||
homeassistant/components/sensor/luftdaten.py
|
homeassistant/components/sensor/luftdaten.py
|
||||||
homeassistant/components/sensor/lyft.py
|
homeassistant/components/sensor/lyft.py
|
||||||
|
homeassistant/components/sensor/magicseaweed.py
|
||||||
homeassistant/components/sensor/metoffice.py
|
homeassistant/components/sensor/metoffice.py
|
||||||
homeassistant/components/sensor/miflora.py
|
homeassistant/components/sensor/miflora.py
|
||||||
homeassistant/components/sensor/mitemp_bt.py
|
homeassistant/components/sensor/mitemp_bt.py
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# Contributing to Home Assistant
|
# 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.
|
The process is straight-forward.
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ import subprocess
|
||||||
import sys
|
import sys
|
||||||
import threading
|
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
|
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."""
|
"""Attempt to use uvloop."""
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
|
@ -280,11 +280,11 @@ def setup_and_run_hass(config_dir: str,
|
||||||
# Imported here to avoid importing asyncio before monkey patch
|
# Imported here to avoid importing asyncio before monkey patch
|
||||||
from homeassistant.util.async_ import run_callback_threadsafe
|
from homeassistant.util.async_ import run_callback_threadsafe
|
||||||
|
|
||||||
def open_browser(event):
|
def open_browser(_: Any) -> None:
|
||||||
"""Open the webinterface in a browser."""
|
"""Open the web interface in a browser."""
|
||||||
if hass.config.api is not None:
|
if hass.config.api is not None: # type: ignore
|
||||||
import webbrowser
|
import webbrowser
|
||||||
webbrowser.open(hass.config.api.base_url)
|
webbrowser.open(hass.config.api.base_url) # type: ignore
|
||||||
|
|
||||||
run_callback_threadsafe(
|
run_callback_threadsafe(
|
||||||
hass.loop,
|
hass.loop,
|
||||||
|
|
|
@ -3,6 +3,7 @@ import base64
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
import hashlib
|
import hashlib
|
||||||
import hmac
|
import hmac
|
||||||
|
from typing import Dict # noqa: F401 pylint: disable=unused-import
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
|
@ -68,12 +69,12 @@ class Data:
|
||||||
"""Return users."""
|
"""Return users."""
|
||||||
return self._data['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.
|
"""Validate a username and password.
|
||||||
|
|
||||||
Raises InvalidAuth if auth invalid.
|
Raises InvalidAuth if auth invalid.
|
||||||
"""
|
"""
|
||||||
password = self.hash_password(password)
|
hashed = self.hash_password(password)
|
||||||
|
|
||||||
found = None
|
found = None
|
||||||
|
|
||||||
|
@ -84,33 +85,33 @@ class Data:
|
||||||
|
|
||||||
if found is None:
|
if found is None:
|
||||||
# Do one more compare to make timing the same as if user was found.
|
# 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
|
raise InvalidAuth
|
||||||
|
|
||||||
if not hmac.compare_digest(password,
|
if not hmac.compare_digest(hashed,
|
||||||
base64.b64decode(found['password'])):
|
base64.b64decode(found['password'])):
|
||||||
raise InvalidAuth
|
raise InvalidAuth
|
||||||
|
|
||||||
def hash_password(self, password, for_storage=False):
|
def hash_password(self, password: str, for_storage: bool = False) -> bytes:
|
||||||
"""Encode a password."""
|
"""Encode a password."""
|
||||||
hashed = hashlib.pbkdf2_hmac(
|
hashed = hashlib.pbkdf2_hmac(
|
||||||
'sha512', password.encode(), self._data['salt'].encode(), 100000)
|
'sha512', password.encode(), self._data['salt'].encode(), 100000)
|
||||||
if for_storage:
|
if for_storage:
|
||||||
hashed = base64.b64encode(hashed).decode()
|
hashed = base64.b64encode(hashed)
|
||||||
return hashed
|
return hashed
|
||||||
|
|
||||||
def add_auth(self, username, password):
|
def add_auth(self, username: str, password: str) -> None:
|
||||||
"""Add a new authenticated user/pass."""
|
"""Add a new authenticated user/pass."""
|
||||||
if any(user['username'] == username for user in self.users):
|
if any(user['username'] == username for user in self.users):
|
||||||
raise InvalidUser
|
raise InvalidUser
|
||||||
|
|
||||||
self.users.append({
|
self.users.append({
|
||||||
'username': username,
|
'username': username,
|
||||||
'password': self.hash_password(password, True),
|
'password': self.hash_password(password, True).decode(),
|
||||||
})
|
})
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_remove_auth(self, username):
|
def async_remove_auth(self, username: str) -> None:
|
||||||
"""Remove authentication."""
|
"""Remove authentication."""
|
||||||
index = None
|
index = None
|
||||||
for i, user in enumerate(self.users):
|
for i, user in enumerate(self.users):
|
||||||
|
@ -123,14 +124,15 @@ class Data:
|
||||||
|
|
||||||
self.users.pop(index)
|
self.users.pop(index)
|
||||||
|
|
||||||
def change_password(self, username, new_password):
|
def change_password(self, username: str, new_password: str) -> None:
|
||||||
"""Update the password.
|
"""Update the password.
|
||||||
|
|
||||||
Raises InvalidUser if user cannot be found.
|
Raises InvalidUser if user cannot be found.
|
||||||
"""
|
"""
|
||||||
for user in self.users:
|
for user in self.users:
|
||||||
if user['username'] == username:
|
if user['username'] == username:
|
||||||
user['password'] = self.hash_password(new_password, True)
|
user['password'] = self.hash_password(
|
||||||
|
new_password, True).decode()
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
raise InvalidUser
|
raise InvalidUser
|
||||||
|
@ -160,7 +162,7 @@ class HassAuthProvider(AuthProvider):
|
||||||
"""Return a flow to login."""
|
"""Return a flow to login."""
|
||||||
return LoginFlow(self)
|
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."""
|
"""Helper to validate a username and password."""
|
||||||
if self.data is None:
|
if self.data is None:
|
||||||
await self.async_initialize()
|
await self.async_initialize()
|
||||||
|
@ -225,7 +227,7 @@ class LoginFlow(data_entry_flow.FlowHandler):
|
||||||
data=user_input
|
data=user_input
|
||||||
)
|
)
|
||||||
|
|
||||||
schema = OrderedDict()
|
schema = OrderedDict() # type: Dict[str, type]
|
||||||
schema['username'] = str
|
schema['username'] = str
|
||||||
schema['password'] = str
|
schema['password'] = str
|
||||||
|
|
||||||
|
|
|
@ -221,8 +221,8 @@ async def async_from_config_file(config_path: str,
|
||||||
@core.callback
|
@core.callback
|
||||||
def async_enable_logging(hass: core.HomeAssistant,
|
def async_enable_logging(hass: core.HomeAssistant,
|
||||||
verbose: bool = False,
|
verbose: bool = False,
|
||||||
log_rotate_days=None,
|
log_rotate_days: Optional[int] = None,
|
||||||
log_file=None,
|
log_file: Optional[str] = None,
|
||||||
log_no_color: bool = False) -> None:
|
log_no_color: bool = False) -> None:
|
||||||
"""Set up the logging.
|
"""Set up the logging.
|
||||||
|
|
||||||
|
@ -291,9 +291,9 @@ def async_enable_logging(hass: core.HomeAssistant,
|
||||||
|
|
||||||
async_handler = AsyncHandler(hass.loop, err_handler)
|
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."""
|
"""Cleanup async handler."""
|
||||||
logging.getLogger('').removeHandler(async_handler)
|
logging.getLogger('').removeHandler(async_handler) # type: ignore
|
||||||
await async_handler.async_close(blocking=True)
|
await async_handler.async_close(blocking=True)
|
||||||
|
|
||||||
hass.bus.async_listen_once(
|
hass.bus.async_listen_once(
|
||||||
|
|
|
@ -167,7 +167,7 @@ def async_setup(hass, config):
|
||||||
def async_handle_core_service(call):
|
def async_handle_core_service(call):
|
||||||
"""Service handler for handling core services."""
|
"""Service handler for handling core services."""
|
||||||
if call.service == SERVICE_HOMEASSISTANT_STOP:
|
if call.service == SERVICE_HOMEASSISTANT_STOP:
|
||||||
hass.async_add_job(hass.async_stop())
|
hass.async_create_task(hass.async_stop())
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -183,7 +183,7 @@ def async_setup(hass, config):
|
||||||
return
|
return
|
||||||
|
|
||||||
if call.service == SERVICE_HOMEASSISTANT_RESTART:
|
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(
|
hass.services.async_register(
|
||||||
ha.DOMAIN, SERVICE_HOMEASSISTANT_STOP, async_handle_core_service)
|
ha.DOMAIN, SERVICE_HOMEASSISTANT_STOP, async_handle_core_service)
|
||||||
|
|
|
@ -85,7 +85,7 @@ ABODE_PLATFORMS = [
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class AbodeSystem(object):
|
class AbodeSystem:
|
||||||
"""Abode System class."""
|
"""Abode System class."""
|
||||||
|
|
||||||
def __init__(self, username, password, cache,
|
def __init__(self, username, password, cache,
|
||||||
|
|
|
@ -110,7 +110,7 @@ NotificationItem = namedtuple(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class AdsHub(object):
|
class AdsHub:
|
||||||
"""Representation of an ADS connection."""
|
"""Representation of an ADS connection."""
|
||||||
|
|
||||||
def __init__(self, ads_client):
|
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.
|
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):
|
def alarm_arm_home(self, code=None):
|
||||||
"""Send arm home command."""
|
"""Send arm home command."""
|
||||||
|
@ -198,7 +198,7 @@ class AlarmControlPanel(Entity):
|
||||||
|
|
||||||
This method must be run in the event loop and returns a coroutine.
|
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):
|
def alarm_arm_away(self, code=None):
|
||||||
"""Send arm away command."""
|
"""Send arm away command."""
|
||||||
|
@ -209,7 +209,7 @@ class AlarmControlPanel(Entity):
|
||||||
|
|
||||||
This method must be run in the event loop and returns a coroutine.
|
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):
|
def alarm_arm_night(self, code=None):
|
||||||
"""Send arm night command."""
|
"""Send arm night command."""
|
||||||
|
@ -220,7 +220,7 @@ class AlarmControlPanel(Entity):
|
||||||
|
|
||||||
This method must be run in the event loop and returns a coroutine.
|
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):
|
def alarm_trigger(self, code=None):
|
||||||
"""Send alarm trigger command."""
|
"""Send alarm trigger command."""
|
||||||
|
@ -231,7 +231,7 @@ class AlarmControlPanel(Entity):
|
||||||
|
|
||||||
This method must be run in the event loop and returns a coroutine.
|
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):
|
def alarm_arm_custom_bypass(self, code=None):
|
||||||
"""Send arm custom bypass command."""
|
"""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.
|
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
|
@property
|
||||||
def state_attributes(self):
|
def state_attributes(self):
|
||||||
|
|
|
@ -83,7 +83,7 @@ class AlarmDotCom(alarm.AlarmControlPanel):
|
||||||
"""Return one or more digits/characters."""
|
"""Return one or more digits/characters."""
|
||||||
if self._code is None:
|
if self._code is None:
|
||||||
return 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 'Number'
|
||||||
return 'Any'
|
return 'Any'
|
||||||
|
|
||||||
|
@ -92,9 +92,9 @@ class AlarmDotCom(alarm.AlarmControlPanel):
|
||||||
"""Return the state of the device."""
|
"""Return the state of the device."""
|
||||||
if self._alarm.state.lower() == 'disarmed':
|
if self._alarm.state.lower() == 'disarmed':
|
||||||
return STATE_ALARM_DISARMED
|
return STATE_ALARM_DISARMED
|
||||||
elif self._alarm.state.lower() == 'armed stay':
|
if self._alarm.state.lower() == 'armed stay':
|
||||||
return STATE_ALARM_ARMED_HOME
|
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_ALARM_ARMED_AWAY
|
||||||
return STATE_UNKNOWN
|
return STATE_UNKNOWN
|
||||||
|
|
||||||
|
|
|
@ -122,10 +122,10 @@ class ArloBaseStation(AlarmControlPanel):
|
||||||
"""Convert Arlo mode to Home Assistant state."""
|
"""Convert Arlo mode to Home Assistant state."""
|
||||||
if mode == ARMED:
|
if mode == ARMED:
|
||||||
return STATE_ALARM_ARMED_AWAY
|
return STATE_ALARM_ARMED_AWAY
|
||||||
elif mode == DISARMED:
|
if mode == DISARMED:
|
||||||
return STATE_ALARM_DISARMED
|
return STATE_ALARM_DISARMED
|
||||||
elif mode == self._home_mode_name:
|
if mode == self._home_mode_name:
|
||||||
return STATE_ALARM_ARMED_HOME
|
return STATE_ALARM_ARMED_HOME
|
||||||
elif mode == self._away_mode_name:
|
if mode == self._away_mode_name:
|
||||||
return STATE_ALARM_ARMED_AWAY
|
return STATE_ALARM_ARMED_AWAY
|
||||||
return mode
|
return mode
|
||||||
|
|
|
@ -55,9 +55,9 @@ class CanaryAlarm(AlarmControlPanel):
|
||||||
mode = location.mode
|
mode = location.mode
|
||||||
if mode.name == LOCATION_MODE_AWAY:
|
if mode.name == LOCATION_MODE_AWAY:
|
||||||
return STATE_ALARM_ARMED_AWAY
|
return STATE_ALARM_ARMED_AWAY
|
||||||
elif mode.name == LOCATION_MODE_HOME:
|
if mode.name == LOCATION_MODE_HOME:
|
||||||
return STATE_ALARM_ARMED_HOME
|
return STATE_ALARM_ARMED_HOME
|
||||||
elif mode.name == LOCATION_MODE_NIGHT:
|
if mode.name == LOCATION_MODE_NIGHT:
|
||||||
return STATE_ALARM_ARMED_NIGHT
|
return STATE_ALARM_ARMED_NIGHT
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ For more details about this platform, please refer to the documentation
|
||||||
https://home-assistant.io/components/demo/
|
https://home-assistant.io/components/demo/
|
||||||
"""
|
"""
|
||||||
import datetime
|
import datetime
|
||||||
import homeassistant.components.alarm_control_panel.manual as manual
|
from homeassistant.components.alarm_control_panel import manual
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_CUSTOM_BYPASS,
|
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_CUSTOM_BYPASS,
|
||||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT,
|
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT,
|
||||||
|
|
|
@ -20,7 +20,6 @@ DEPENDENCIES = ['homematicip_cloud']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
HMIP_OPEN = 'OPEN'
|
|
||||||
HMIP_ZONE_AWAY = 'EXTERNAL'
|
HMIP_ZONE_AWAY = 'EXTERNAL'
|
||||||
HMIP_ZONE_HOME = 'INTERNAL'
|
HMIP_ZONE_HOME = 'INTERNAL'
|
||||||
|
|
||||||
|
@ -57,14 +56,18 @@ class HomematicipSecurityZone(HomematicipGenericDevice, AlarmControlPanel):
|
||||||
@property
|
@property
|
||||||
def state(self):
|
def state(self):
|
||||||
"""Return the state of the device."""
|
"""Return the state of the device."""
|
||||||
|
from homematicip.base.enums import WindowState
|
||||||
|
|
||||||
if self._device.active:
|
if self._device.active:
|
||||||
if (self._device.sabotage or self._device.motionDetected or
|
if (self._device.sabotage or self._device.motionDetected or
|
||||||
self._device.windowState == HMIP_OPEN):
|
self._device.windowState == WindowState.OPEN):
|
||||||
return STATE_ALARM_TRIGGERED
|
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_HOME
|
||||||
return STATE_ALARM_ARMED_AWAY
|
|
||||||
|
|
||||||
return STATE_ALARM_DISARMED
|
return STATE_ALARM_DISARMED
|
||||||
|
|
||||||
|
@ -79,10 +82,3 @@ class HomematicipSecurityZone(HomematicipGenericDevice, AlarmControlPanel):
|
||||||
async def async_alarm_arm_away(self, code=None):
|
async def async_alarm_arm_away(self, code=None):
|
||||||
"""Send arm away command."""
|
"""Send arm away command."""
|
||||||
await self._home.set_security_zones_activation(True, True)
|
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."""
|
"""Return one or more digits/characters."""
|
||||||
if self._code is None:
|
if self._code is None:
|
||||||
return 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 'Number'
|
||||||
return 'Any'
|
return 'Any'
|
||||||
|
|
||||||
|
|
|
@ -205,7 +205,7 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
||||||
"""Return one or more digits/characters."""
|
"""Return one or more digits/characters."""
|
||||||
if self._code is None:
|
if self._code is None:
|
||||||
return 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 'Number'
|
||||||
return 'Any'
|
return 'Any'
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@ from homeassistant.const import (
|
||||||
STATE_ALARM_DISARMED, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED,
|
STATE_ALARM_DISARMED, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED,
|
||||||
CONF_PLATFORM, CONF_NAME, CONF_CODE, CONF_DELAY_TIME, CONF_PENDING_TIME,
|
CONF_PLATFORM, CONF_NAME, CONF_CODE, CONF_DELAY_TIME, CONF_PENDING_TIME,
|
||||||
CONF_TRIGGER_TIME, CONF_DISARM_AFTER_TRIGGER)
|
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.helpers.event import async_track_state_change
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
|
@ -241,7 +241,7 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
|
||||||
"""Return one or more digits/characters."""
|
"""Return one or more digits/characters."""
|
||||||
if self._code is None:
|
if self._code is None:
|
||||||
return 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 'Number'
|
||||||
return 'Any'
|
return 'Any'
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@ import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
import homeassistant.components.alarm_control_panel as alarm
|
import homeassistant.components.alarm_control_panel as alarm
|
||||||
import homeassistant.components.mqtt as mqtt
|
from homeassistant.components import mqtt
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
|
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
|
||||||
STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, STATE_UNKNOWN,
|
STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, STATE_UNKNOWN,
|
||||||
|
@ -49,6 +49,9 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||||
"""Set up the MQTT Alarm Control Panel platform."""
|
"""Set up the MQTT Alarm Control Panel platform."""
|
||||||
|
if discovery_info is not None:
|
||||||
|
config = PLATFORM_SCHEMA(discovery_info)
|
||||||
|
|
||||||
async_add_devices([MqttAlarm(
|
async_add_devices([MqttAlarm(
|
||||||
config.get(CONF_NAME),
|
config.get(CONF_NAME),
|
||||||
config.get(CONF_STATE_TOPIC),
|
config.get(CONF_STATE_TOPIC),
|
||||||
|
@ -123,7 +126,7 @@ class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel):
|
||||||
"""Return one or more digits/characters."""
|
"""Return one or more digits/characters."""
|
||||||
if self._code is None:
|
if self._code is None:
|
||||||
return 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 'Number'
|
||||||
return 'Any'
|
return 'Any'
|
||||||
|
|
||||||
|
|
|
@ -9,23 +9,22 @@ import re
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
import homeassistant.components.alarm_control_panel as alarm
|
from homeassistant.components.alarm_control_panel import (
|
||||||
from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA
|
PLATFORM_SCHEMA, AlarmControlPanel)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_CODE, CONF_NAME, CONF_PASSWORD, CONF_USERNAME,
|
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)
|
STATE_ALARM_DISARMED, STATE_UNKNOWN)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
REQUIREMENTS = ['simplisafe-python==1.0.5']
|
REQUIREMENTS = ['simplisafe-python==2.0.2']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DEFAULT_NAME = 'SimpliSafe'
|
DEFAULT_NAME = 'SimpliSafe'
|
||||||
DOMAIN = 'simplisafe'
|
|
||||||
|
|
||||||
NOTIFICATION_ID = 'simplisafe_notification'
|
ATTR_ALARM_ACTIVE = "alarm_active"
|
||||||
NOTIFICATION_TITLE = 'SimpliSafe Setup'
|
ATTR_TEMPERATURE = "temperature"
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
vol.Required(CONF_PASSWORD): cv.string,
|
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):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
"""Set up the SimpliSafe platform."""
|
"""Set up the SimpliSafe platform."""
|
||||||
from simplipy.api import SimpliSafeApiInterface, get_systems
|
from simplipy.api import SimpliSafeApiInterface, SimpliSafeAPIException
|
||||||
name = config.get(CONF_NAME)
|
name = config.get(CONF_NAME)
|
||||||
code = config.get(CONF_CODE)
|
code = config.get(CONF_CODE)
|
||||||
username = config.get(CONF_USERNAME)
|
username = config.get(CONF_USERNAME)
|
||||||
password = config.get(CONF_PASSWORD)
|
password = config.get(CONF_PASSWORD)
|
||||||
|
|
||||||
simplisafe = SimpliSafeApiInterface()
|
try:
|
||||||
status = simplisafe.set_credentials(username, password)
|
simplisafe = SimpliSafeApiInterface(username, password)
|
||||||
if status:
|
except SimpliSafeAPIException:
|
||||||
hass.data[DOMAIN] = simplisafe
|
_LOGGER.error("Failed to setup SimpliSafe")
|
||||||
locations = get_systems(simplisafe)
|
return
|
||||||
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
|
|
||||||
|
|
||||||
def logout(event):
|
systems = []
|
||||||
"""Logout of the SimpliSafe API."""
|
|
||||||
hass.data[DOMAIN].logout()
|
|
||||||
|
|
||||||
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."""
|
"""Representation of a SimpliSafe alarm."""
|
||||||
|
|
||||||
def __init__(self, simplisafe, name, code):
|
def __init__(self, simplisafe, name, code):
|
||||||
|
@ -75,31 +65,37 @@ class SimpliSafeAlarm(alarm.AlarmControlPanel):
|
||||||
self._name = name
|
self._name = name
|
||||||
self._code = str(code) if code else None
|
self._code = str(code) if code else None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unique_id(self):
|
||||||
|
"""Return the unique ID."""
|
||||||
|
return self.simplisafe.location_id
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Return the name of the device."""
|
"""Return the name of the device."""
|
||||||
if self._name is not None:
|
if self._name is not None:
|
||||||
return self._name
|
return self._name
|
||||||
return 'Alarm {}'.format(self.simplisafe.location_id())
|
return 'Alarm {}'.format(self.simplisafe.location_id)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def code_format(self):
|
def code_format(self):
|
||||||
"""Return one or more digits/characters."""
|
"""Return one or more digits/characters."""
|
||||||
if self._code is None:
|
if self._code is None:
|
||||||
return 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 'Number'
|
||||||
return 'Any'
|
return 'Any'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self):
|
def state(self):
|
||||||
"""Return the state of the device."""
|
"""Return the state of the device."""
|
||||||
status = self.simplisafe.state()
|
status = self.simplisafe.state
|
||||||
if status == 'off':
|
if status.lower() == 'off':
|
||||||
state = STATE_ALARM_DISARMED
|
state = STATE_ALARM_DISARMED
|
||||||
elif status == 'home':
|
elif status.lower() == 'home' or status.lower() == 'home_count':
|
||||||
state = STATE_ALARM_ARMED_HOME
|
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
|
state = STATE_ALARM_ARMED_AWAY
|
||||||
else:
|
else:
|
||||||
state = STATE_UNKNOWN
|
state = STATE_UNKNOWN
|
||||||
|
@ -108,14 +104,13 @@ class SimpliSafeAlarm(alarm.AlarmControlPanel):
|
||||||
@property
|
@property
|
||||||
def device_state_attributes(self):
|
def device_state_attributes(self):
|
||||||
"""Return the state attributes."""
|
"""Return the state attributes."""
|
||||||
return {
|
attributes = {}
|
||||||
'alarm': self.simplisafe.alarm(),
|
|
||||||
'co': self.simplisafe.carbon_monoxide(),
|
attributes[ATTR_ALARM_ACTIVE] = self.simplisafe.alarm_active
|
||||||
'fire': self.simplisafe.fire(),
|
if self.simplisafe.temperature is not None:
|
||||||
'flood': self.simplisafe.flood(),
|
attributes[ATTR_TEMPERATURE] = self.simplisafe.temperature
|
||||||
'last_event': self.simplisafe.last_event(),
|
|
||||||
'temperature': self.simplisafe.temperature(),
|
return attributes
|
||||||
}
|
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
"""Update alarm status."""
|
"""Update alarm status."""
|
||||||
|
|
|
@ -34,6 +34,8 @@ CONF_ZONE_NAME = 'name'
|
||||||
CONF_ZONE_TYPE = 'type'
|
CONF_ZONE_TYPE = 'type'
|
||||||
CONF_ZONE_RFID = 'rfid'
|
CONF_ZONE_RFID = 'rfid'
|
||||||
CONF_ZONES = 'zones'
|
CONF_ZONES = 'zones'
|
||||||
|
CONF_RELAY_ADDR = 'relayaddr'
|
||||||
|
CONF_RELAY_CHAN = 'relaychan'
|
||||||
|
|
||||||
DEFAULT_DEVICE_TYPE = 'socket'
|
DEFAULT_DEVICE_TYPE = 'socket'
|
||||||
DEFAULT_DEVICE_HOST = 'localhost'
|
DEFAULT_DEVICE_HOST = 'localhost'
|
||||||
|
@ -53,6 +55,7 @@ SIGNAL_PANEL_DISARM = 'alarmdecoder.panel_disarm'
|
||||||
SIGNAL_ZONE_FAULT = 'alarmdecoder.zone_fault'
|
SIGNAL_ZONE_FAULT = 'alarmdecoder.zone_fault'
|
||||||
SIGNAL_ZONE_RESTORE = 'alarmdecoder.zone_restore'
|
SIGNAL_ZONE_RESTORE = 'alarmdecoder.zone_restore'
|
||||||
SIGNAL_RFX_MESSAGE = 'alarmdecoder.rfx_message'
|
SIGNAL_RFX_MESSAGE = 'alarmdecoder.rfx_message'
|
||||||
|
SIGNAL_REL_MESSAGE = 'alarmdecoder.rel_message'
|
||||||
|
|
||||||
DEVICE_SOCKET_SCHEMA = vol.Schema({
|
DEVICE_SOCKET_SCHEMA = vol.Schema({
|
||||||
vol.Required(CONF_DEVICE_TYPE): 'socket',
|
vol.Required(CONF_DEVICE_TYPE): 'socket',
|
||||||
|
@ -71,7 +74,11 @@ ZONE_SCHEMA = vol.Schema({
|
||||||
vol.Required(CONF_ZONE_NAME): cv.string,
|
vol.Required(CONF_ZONE_NAME): cv.string,
|
||||||
vol.Optional(CONF_ZONE_TYPE,
|
vol.Optional(CONF_ZONE_TYPE,
|
||||||
default=DEFAULT_ZONE_TYPE): vol.Any(DEVICE_CLASSES_SCHEMA),
|
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({
|
CONFIG_SCHEMA = vol.Schema({
|
||||||
DOMAIN: vol.Schema({
|
DOMAIN: vol.Schema({
|
||||||
|
@ -153,6 +160,11 @@ def setup(hass, config):
|
||||||
hass.helpers.dispatcher.dispatcher_send(
|
hass.helpers.dispatcher.dispatcher_send(
|
||||||
SIGNAL_ZONE_RESTORE, zone)
|
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
|
controller = False
|
||||||
if device_type == 'socket':
|
if device_type == 'socket':
|
||||||
host = device.get(CONF_DEVICE_HOST)
|
host = device.get(CONF_DEVICE_HOST)
|
||||||
|
@ -171,6 +183,7 @@ def setup(hass, config):
|
||||||
controller.on_zone_fault += zone_fault_callback
|
controller.on_zone_fault += zone_fault_callback
|
||||||
controller.on_zone_restore += zone_restore_callback
|
controller.on_zone_restore += zone_restore_callback
|
||||||
controller.on_close += handle_closed_connection
|
controller.on_close += handle_closed_connection
|
||||||
|
controller.on_relay_changed += handle_rel_message
|
||||||
|
|
||||||
hass.data[DATA_AD] = controller
|
hass.data[DATA_AD] = controller
|
||||||
|
|
||||||
|
|
|
@ -68,7 +68,7 @@ def turn_on(hass, entity_id):
|
||||||
def async_turn_on(hass, entity_id):
|
def async_turn_on(hass, entity_id):
|
||||||
"""Async reset the alert."""
|
"""Async reset the alert."""
|
||||||
data = {ATTR_ENTITY_ID: entity_id}
|
data = {ATTR_ENTITY_ID: entity_id}
|
||||||
hass.async_add_job(
|
hass.async_create_task(
|
||||||
hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data))
|
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):
|
def async_turn_off(hass, entity_id):
|
||||||
"""Async acknowledge the alert."""
|
"""Async acknowledge the alert."""
|
||||||
data = {ATTR_ENTITY_ID: entity_id}
|
data = {ATTR_ENTITY_ID: entity_id}
|
||||||
hass.async_add_job(
|
hass.async_create_task(
|
||||||
hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, data))
|
hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, data))
|
||||||
|
|
||||||
|
|
||||||
|
@ -94,7 +94,7 @@ def toggle(hass, entity_id):
|
||||||
def async_toggle(hass, entity_id):
|
def async_toggle(hass, entity_id):
|
||||||
"""Async toggle acknowledgement of alert."""
|
"""Async toggle acknowledgement of alert."""
|
||||||
data = {ATTR_ENTITY_ID: entity_id}
|
data = {ATTR_ENTITY_ID: entity_id}
|
||||||
hass.async_add_job(
|
hass.async_create_task(
|
||||||
hass.services.async_call(DOMAIN, SERVICE_TOGGLE, data))
|
hass.services.async_call(DOMAIN, SERVICE_TOGGLE, data))
|
||||||
|
|
||||||
|
|
||||||
|
@ -217,7 +217,7 @@ class Alert(ToggleEntity):
|
||||||
else:
|
else:
|
||||||
yield from self._schedule_notify()
|
yield from self._schedule_notify()
|
||||||
|
|
||||||
self.hass.async_add_job(self.async_update_ha_state)
|
self.async_schedule_update_ha_state()
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def end_alerting(self):
|
def end_alerting(self):
|
||||||
|
@ -228,7 +228,7 @@ class Alert(ToggleEntity):
|
||||||
self._firing = False
|
self._firing = False
|
||||||
if self._done_message and self._send_done_message:
|
if self._done_message and self._send_done_message:
|
||||||
yield from self._notify_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
|
@asyncio.coroutine
|
||||||
def _schedule_notify(self):
|
def _schedule_notify(self):
|
||||||
|
|
|
@ -210,7 +210,7 @@ def resolve_slot_synonyms(key, request):
|
||||||
return resolved_value
|
return resolved_value
|
||||||
|
|
||||||
|
|
||||||
class AlexaResponse(object):
|
class AlexaResponse:
|
||||||
"""Help generating the response for Alexa."""
|
"""Help generating the response for Alexa."""
|
||||||
|
|
||||||
def __init__(self, hass, intent_info):
|
def __init__(self, hass, intent_info):
|
||||||
|
|
|
@ -55,7 +55,7 @@ HANDLERS = Registry()
|
||||||
ENTITY_ADAPTERS = Registry()
|
ENTITY_ADAPTERS = Registry()
|
||||||
|
|
||||||
|
|
||||||
class _DisplayCategory(object):
|
class _DisplayCategory:
|
||||||
"""Possible display categories for Discovery response.
|
"""Possible display categories for Discovery response.
|
||||||
|
|
||||||
https://developer.amazon.com/docs/device-apis/alexa-discovery.html#display-categories
|
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."""
|
"""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.
|
"""An adaptation of an entity, expressed in Alexa's terms.
|
||||||
|
|
||||||
The API handlers should manipulate entities only through this interface.
|
The API handlers should manipulate entities only through this interface.
|
||||||
|
@ -208,7 +208,7 @@ class _AlexaEntity(object):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
class _AlexaInterface(object):
|
class _AlexaInterface:
|
||||||
def __init__(self, entity):
|
def __init__(self, entity):
|
||||||
self.entity = entity
|
self.entity = entity
|
||||||
|
|
||||||
|
@ -315,7 +315,7 @@ class _AlexaLockController(_AlexaInterface):
|
||||||
|
|
||||||
if self.entity.state == STATE_LOCKED:
|
if self.entity.state == STATE_LOCKED:
|
||||||
return 'LOCKED'
|
return 'LOCKED'
|
||||||
elif self.entity.state == STATE_UNLOCKED:
|
if self.entity.state == STATE_UNLOCKED:
|
||||||
return 'UNLOCKED'
|
return 'UNLOCKED'
|
||||||
return 'JAMMED'
|
return 'JAMMED'
|
||||||
|
|
||||||
|
@ -615,7 +615,7 @@ class _SensorCapabilities(_AlexaEntity):
|
||||||
yield _AlexaTemperatureSensor(self.entity)
|
yield _AlexaTemperatureSensor(self.entity)
|
||||||
|
|
||||||
|
|
||||||
class _Cause(object):
|
class _Cause:
|
||||||
"""Possible causes for property changes.
|
"""Possible causes for property changes.
|
||||||
|
|
||||||
https://developer.amazon.com/docs/smarthome/state-reporting-for-a-smart-home-skill.html#cause-object
|
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
|
return True
|
||||||
|
|
||||||
|
|
||||||
class AmcrestDevice(object):
|
class AmcrestDevice:
|
||||||
"""Representation of a base Amcrest discovery device."""
|
"""Representation of a base Amcrest discovery device."""
|
||||||
|
|
||||||
def __init__(self, camera, name, authentication, ffmpeg_arguments,
|
def __init__(self, camera, name, authentication, ffmpeg_arguments,
|
||||||
|
|
|
@ -214,11 +214,11 @@ def async_setup(hass, config):
|
||||||
CONF_PASSWORD: password
|
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))
|
hass, 'camera', 'mjpeg', mjpeg_camera, config))
|
||||||
|
|
||||||
if sensors:
|
if sensors:
|
||||||
hass.async_add_job(discovery.async_load_platform(
|
hass.async_create_task(discovery.async_load_platform(
|
||||||
hass, 'sensor', DOMAIN, {
|
hass, 'sensor', DOMAIN, {
|
||||||
CONF_NAME: name,
|
CONF_NAME: name,
|
||||||
CONF_HOST: host,
|
CONF_HOST: host,
|
||||||
|
@ -226,7 +226,7 @@ def async_setup(hass, config):
|
||||||
}, config))
|
}, config))
|
||||||
|
|
||||||
if switches:
|
if switches:
|
||||||
hass.async_add_job(discovery.async_load_platform(
|
hass.async_create_task(discovery.async_load_platform(
|
||||||
hass, 'switch', DOMAIN, {
|
hass, 'switch', DOMAIN, {
|
||||||
CONF_NAME: name,
|
CONF_NAME: name,
|
||||||
CONF_HOST: host,
|
CONF_HOST: host,
|
||||||
|
@ -234,7 +234,7 @@ def async_setup(hass, config):
|
||||||
}, config))
|
}, config))
|
||||||
|
|
||||||
if motion:
|
if motion:
|
||||||
hass.async_add_job(discovery.async_load_platform(
|
hass.async_create_task(discovery.async_load_platform(
|
||||||
hass, 'binary_sensor', DOMAIN, {
|
hass, 'binary_sensor', DOMAIN, {
|
||||||
CONF_HOST: host,
|
CONF_HOST: host,
|
||||||
CONF_NAME: name,
|
CONF_NAME: name,
|
||||||
|
|
|
@ -58,7 +58,7 @@ def setup(hass, config):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class APCUPSdData(object):
|
class APCUPSdData:
|
||||||
"""Stores the data retrieved from APCUPSd.
|
"""Stores the data retrieved from APCUPSd.
|
||||||
|
|
||||||
For each entity to use, acts as the single point responsible for fetching
|
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
|
is_new_state = hass.states.get(entity_id) is None
|
||||||
|
|
||||||
# Write state
|
# 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
|
# Read the state back for our response
|
||||||
status_code = HTTP_CREATED if is_new_state else 200
|
status_code = HTTP_CREATED if is_new_state else 200
|
||||||
|
@ -279,7 +280,8 @@ class APIEventView(HomeAssistantView):
|
||||||
event_data[key] = state
|
event_data[key] = state
|
||||||
|
|
||||||
request.app['hass'].bus.async_fire(
|
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))
|
return self.json_message("Event {} fired.".format(event_type))
|
||||||
|
|
||||||
|
@ -316,7 +318,8 @@ class APIDomainServicesView(HomeAssistantView):
|
||||||
"Data should be valid JSON.", HTTP_BAD_REQUEST)
|
"Data should be valid JSON.", HTTP_BAD_REQUEST)
|
||||||
|
|
||||||
with AsyncTrackStates(hass) as changed_states:
|
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)
|
return self.json(changed_states)
|
||||||
|
|
||||||
|
|
|
@ -45,7 +45,7 @@ NOTIFICATION_AUTH_TITLE = 'Apple TV Authentication'
|
||||||
NOTIFICATION_SCAN_ID = 'apple_tv_scan_notification'
|
NOTIFICATION_SCAN_ID = 'apple_tv_scan_notification'
|
||||||
NOTIFICATION_SCAN_TITLE = 'Apple TV Scan'
|
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
|
# 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
|
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, '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))
|
hass, 'remote', DOMAIN, atv_config))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -62,7 +62,7 @@ def setup(hass, config):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class ArduinoBoard(object):
|
class ArduinoBoard:
|
||||||
"""Representation of an Arduino board."""
|
"""Representation of an Arduino board."""
|
||||||
|
|
||||||
def __init__(self, port):
|
def __init__(self, port):
|
||||||
|
|
|
@ -16,7 +16,7 @@ from homeassistant.const import (
|
||||||
from homeassistant.helpers.event import track_time_interval
|
from homeassistant.helpers.event import track_time_interval
|
||||||
from homeassistant.helpers.dispatcher import dispatcher_send
|
from homeassistant.helpers.dispatcher import dispatcher_send
|
||||||
|
|
||||||
REQUIREMENTS = ['pyarlo==0.1.9']
|
REQUIREMENTS = ['pyarlo==0.2.0']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
|
@ -48,7 +48,7 @@ def setup(hass, config):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class AsteriskData(object):
|
class AsteriskData:
|
||||||
"""Store Asterisk mailbox data."""
|
"""Store Asterisk mailbox data."""
|
||||||
|
|
||||||
def __init__(self, hass, host, port, password):
|
def __init__(self, hass, host, port, password):
|
||||||
|
|
|
@ -123,9 +123,9 @@ def setup_august(hass, config, api, authenticator):
|
||||||
discovery.load_platform(hass, component, DOMAIN, {}, config)
|
discovery.load_platform(hass, component, DOMAIN, {}, config)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
elif state == AuthenticationState.BAD_PASSWORD:
|
if state == AuthenticationState.BAD_PASSWORD:
|
||||||
return False
|
return False
|
||||||
elif state == AuthenticationState.REQUIRES_VALIDATION:
|
if state == AuthenticationState.REQUIRES_VALIDATION:
|
||||||
request_configuration(hass, config, api, authenticator)
|
request_configuration(hass, config, api, authenticator)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
|
@ -1,62 +1,5 @@
|
||||||
"""Component to allow users to login and get tokens.
|
"""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
|
# POST /auth/token
|
||||||
|
|
||||||
This is an OAuth2 endpoint for granting tokens. We currently support the grant
|
This is an OAuth2 endpoint for granting tokens. We currently support the grant
|
||||||
|
@ -102,24 +45,20 @@ a limited expiration.
|
||||||
"token_type": "Bearer"
|
"token_type": "Bearer"
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
from datetime import timedelta
|
|
||||||
import logging
|
import logging
|
||||||
import uuid
|
import uuid
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
import aiohttp.web
|
|
||||||
import voluptuous as vol
|
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 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.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 homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
from . import indieauth
|
from . import indieauth
|
||||||
|
from . import login_flow
|
||||||
|
|
||||||
DOMAIN = 'auth'
|
DOMAIN = 'auth'
|
||||||
DEPENDENCIES = ['http']
|
DEPENDENCIES = ['http']
|
||||||
|
@ -136,10 +75,6 @@ async def async_setup(hass, config):
|
||||||
"""Component to allow users to login."""
|
"""Component to allow users to login."""
|
||||||
store_credentials, retrieve_credentials = _create_cred_store()
|
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(GrantTokenView(retrieve_credentials))
|
||||||
hass.http.register_view(LinkUserView(retrieve_credentials))
|
hass.http.register_view(LinkUserView(retrieve_credentials))
|
||||||
|
|
||||||
|
@ -148,93 +83,11 @@ async def async_setup(hass, config):
|
||||||
SCHEMA_WS_CURRENT_USER
|
SCHEMA_WS_CURRENT_USER
|
||||||
)
|
)
|
||||||
|
|
||||||
|
await login_flow.async_setup(hass, store_credentials)
|
||||||
|
|
||||||
return True
|
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):
|
class GrantTokenView(HomeAssistantView):
|
||||||
"""View to grant tokens."""
|
"""View to grant tokens."""
|
||||||
|
|
||||||
|
@ -247,11 +100,26 @@ class GrantTokenView(HomeAssistantView):
|
||||||
"""Initialize the grant token view."""
|
"""Initialize the grant token view."""
|
||||||
self._retrieve_credentials = retrieve_credentials
|
self._retrieve_credentials = retrieve_credentials
|
||||||
|
|
||||||
|
@log_invalid_auth
|
||||||
async def post(self, request):
|
async def post(self, request):
|
||||||
"""Grant a token."""
|
"""Grant a token."""
|
||||||
hass = request.app['hass']
|
hass = request.app['hass']
|
||||||
data = await request.post()
|
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')
|
client_id = data.get('client_id')
|
||||||
if client_id is None or not indieauth.verify_client_id(client_id):
|
if client_id is None or not indieauth.verify_client_id(client_id):
|
||||||
return self.json({
|
return self.json({
|
||||||
|
@ -259,21 +127,6 @@ class GrantTokenView(HomeAssistantView):
|
||||||
'error_description': 'Invalid client id',
|
'error_description': 'Invalid client id',
|
||||||
}, status_code=400)
|
}, 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')
|
code = data.get('code')
|
||||||
|
|
||||||
if code is None:
|
if code is None:
|
||||||
|
@ -309,8 +162,15 @@ class GrantTokenView(HomeAssistantView):
|
||||||
int(refresh_token.access_token_expiration.total_seconds()),
|
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."""
|
"""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')
|
token = data.get('refresh_token')
|
||||||
|
|
||||||
if token is None:
|
if token is None:
|
||||||
|
@ -320,11 +180,16 @@ class GrantTokenView(HomeAssistantView):
|
||||||
|
|
||||||
refresh_token = await hass.auth.async_get_refresh_token(token)
|
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({
|
return self.json({
|
||||||
'error': 'invalid_grant',
|
'error': 'invalid_grant',
|
||||||
}, status_code=400)
|
}, 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)
|
access_token = hass.auth.async_create_access_token(refresh_token)
|
||||||
|
|
||||||
return self.json({
|
return self.json({
|
||||||
|
@ -412,4 +277,7 @@ def websocket_current_user(hass, connection, msg):
|
||||||
'id': user.id,
|
'id': user.id,
|
||||||
'name': user.name,
|
'name': user.name,
|
||||||
'is_owner': user.is_owner,
|
'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]
|
||||||
}))
|
}))
|
||||||
|
|
172
homeassistant/components/auth/login_flow.py
Normal file
172
homeassistant/components/auth/login_flow.py
Normal file
|
@ -0,0 +1,172 @@
|
||||||
|
"""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 homeassistant.helpers.data_entry_flow import (
|
||||||
|
FlowManagerIndexView, FlowManagerResourceView)
|
||||||
|
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])
|
||||||
|
|
||||||
|
|
||||||
|
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,
|
||||||
|
}))
|
||||||
|
@log_invalid_auth
|
||||||
|
async def post(self, request, data):
|
||||||
|
"""Create a new login flow."""
|
||||||
|
if not indieauth.verify_redirect_uri(data['client_id'],
|
||||||
|
data['redirect_uri']):
|
||||||
|
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))
|
||||||
|
@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(self._prepare_result_json(result))
|
||||||
|
|
||||||
|
result.pop('data')
|
||||||
|
result['result'] = self._store_credentials(client_id, result['result'])
|
||||||
|
|
||||||
|
return self.json(result)
|
|
@ -297,7 +297,7 @@ class AutomationEntity(ToggleEntity):
|
||||||
return
|
return
|
||||||
|
|
||||||
# HomeAssistant is starting up
|
# HomeAssistant is starting up
|
||||||
elif self.hass.state == CoreState.not_running:
|
if self.hass.state == CoreState.not_running:
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_enable_automation(event):
|
def async_enable_automation(event):
|
||||||
"""Start automation on startup."""
|
"""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
|
# Automation are enabled while hass is starting up, fire right away
|
||||||
# Check state because a config reload shouldn't trigger it.
|
# 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, {
|
hass.async_run_job(action, {
|
||||||
'trigger': {
|
'trigger': {
|
||||||
'platform': 'homeassistant',
|
'platform': 'homeassistant',
|
||||||
|
|
|
@ -10,7 +10,7 @@ import json
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
import homeassistant.components.mqtt as mqtt
|
from homeassistant.components import mqtt
|
||||||
from homeassistant.const import (CONF_PLATFORM, CONF_PAYLOAD)
|
from homeassistant.const import (CONF_PLATFORM, CONF_PAYLOAD)
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@ DOMAIN = 'bbb_gpio'
|
||||||
def setup(hass, config):
|
def setup(hass, config):
|
||||||
"""Set up the BeagleBone Black GPIO component."""
|
"""Set up the BeagleBone Black GPIO component."""
|
||||||
# pylint: disable=import-error
|
# pylint: disable=import-error
|
||||||
import Adafruit_BBIO.GPIO as GPIO
|
from Adafruit_BBIO import GPIO
|
||||||
|
|
||||||
def cleanup_gpio(event):
|
def cleanup_gpio(event):
|
||||||
"""Stuff to do before stopping."""
|
"""Stuff to do before stopping."""
|
||||||
|
@ -36,14 +36,14 @@ def setup(hass, config):
|
||||||
def setup_output(pin):
|
def setup_output(pin):
|
||||||
"""Set up a GPIO as output."""
|
"""Set up a GPIO as output."""
|
||||||
# pylint: disable=import-error
|
# pylint: disable=import-error
|
||||||
import Adafruit_BBIO.GPIO as GPIO
|
from Adafruit_BBIO import GPIO
|
||||||
GPIO.setup(pin, GPIO.OUT)
|
GPIO.setup(pin, GPIO.OUT)
|
||||||
|
|
||||||
|
|
||||||
def setup_input(pin, pull_mode):
|
def setup_input(pin, pull_mode):
|
||||||
"""Set up a GPIO as input."""
|
"""Set up a GPIO as input."""
|
||||||
# pylint: disable=import-error
|
# pylint: disable=import-error
|
||||||
import Adafruit_BBIO.GPIO as GPIO
|
from Adafruit_BBIO import GPIO
|
||||||
GPIO.setup(pin, GPIO.IN,
|
GPIO.setup(pin, GPIO.IN,
|
||||||
GPIO.PUD_DOWN if pull_mode == 'DOWN'
|
GPIO.PUD_DOWN if pull_mode == 'DOWN'
|
||||||
else GPIO.PUD_UP)
|
else GPIO.PUD_UP)
|
||||||
|
@ -52,20 +52,20 @@ def setup_input(pin, pull_mode):
|
||||||
def write_output(pin, value):
|
def write_output(pin, value):
|
||||||
"""Write a value to a GPIO."""
|
"""Write a value to a GPIO."""
|
||||||
# pylint: disable=import-error
|
# pylint: disable=import-error
|
||||||
import Adafruit_BBIO.GPIO as GPIO
|
from Adafruit_BBIO import GPIO
|
||||||
GPIO.output(pin, value)
|
GPIO.output(pin, value)
|
||||||
|
|
||||||
|
|
||||||
def read_input(pin):
|
def read_input(pin):
|
||||||
"""Read a value from a GPIO."""
|
"""Read a value from a GPIO."""
|
||||||
# pylint: disable=import-error
|
# pylint: disable=import-error
|
||||||
import Adafruit_BBIO.GPIO as GPIO
|
from Adafruit_BBIO import GPIO
|
||||||
return GPIO.input(pin) is GPIO.HIGH
|
return GPIO.input(pin) is GPIO.HIGH
|
||||||
|
|
||||||
|
|
||||||
def edge_detect(pin, event_callback, bounce):
|
def edge_detect(pin, event_callback, bounce):
|
||||||
"""Add detection for RISING and FALLING events."""
|
"""Add detection for RISING and FALLING events."""
|
||||||
# pylint: disable=import-error
|
# pylint: disable=import-error
|
||||||
import Adafruit_BBIO.GPIO as GPIO
|
from Adafruit_BBIO import GPIO
|
||||||
GPIO.add_event_detect(
|
GPIO.add_event_detect(
|
||||||
pin, GPIO.BOTH, callback=event_callback, bouncetime=bounce)
|
pin, GPIO.BOTH, callback=event_callback, bouncetime=bounce)
|
||||||
|
|
|
@ -11,7 +11,8 @@ from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||||
from homeassistant.components.alarmdecoder import (
|
from homeassistant.components.alarmdecoder import (
|
||||||
ZONE_SCHEMA, CONF_ZONES, CONF_ZONE_NAME, CONF_ZONE_TYPE,
|
ZONE_SCHEMA, CONF_ZONES, CONF_ZONE_NAME, CONF_ZONE_TYPE,
|
||||||
CONF_ZONE_RFID, SIGNAL_ZONE_FAULT, SIGNAL_ZONE_RESTORE,
|
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']
|
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_type = device_config_data[CONF_ZONE_TYPE]
|
||||||
zone_name = device_config_data[CONF_ZONE_NAME]
|
zone_name = device_config_data[CONF_ZONE_NAME]
|
||||||
zone_rfid = device_config_data.get(CONF_ZONE_RFID)
|
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(
|
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)
|
devices.append(device)
|
||||||
|
|
||||||
add_devices(devices)
|
add_devices(devices)
|
||||||
|
@ -49,7 +52,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
class AlarmDecoderBinarySensor(BinarySensorDevice):
|
class AlarmDecoderBinarySensor(BinarySensorDevice):
|
||||||
"""Representation of an AlarmDecoder binary sensor."""
|
"""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."""
|
"""Initialize the binary_sensor."""
|
||||||
self._zone_number = zone_number
|
self._zone_number = zone_number
|
||||||
self._zone_type = zone_type
|
self._zone_type = zone_type
|
||||||
|
@ -57,6 +61,8 @@ class AlarmDecoderBinarySensor(BinarySensorDevice):
|
||||||
self._name = zone_name
|
self._name = zone_name
|
||||||
self._rfid = zone_rfid
|
self._rfid = zone_rfid
|
||||||
self._rfstate = None
|
self._rfstate = None
|
||||||
|
self._relay_addr = relay_addr
|
||||||
|
self._relay_chan = relay_chan
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_added_to_hass(self):
|
def async_added_to_hass(self):
|
||||||
|
@ -70,6 +76,9 @@ class AlarmDecoderBinarySensor(BinarySensorDevice):
|
||||||
self.hass.helpers.dispatcher.async_dispatcher_connect(
|
self.hass.helpers.dispatcher.async_dispatcher_connect(
|
||||||
SIGNAL_RFX_MESSAGE, self._rfx_message_callback)
|
SIGNAL_RFX_MESSAGE, self._rfx_message_callback)
|
||||||
|
|
||||||
|
self.hass.helpers.dispatcher.async_dispatcher_connect(
|
||||||
|
SIGNAL_REL_MESSAGE, self._rel_message_callback)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Return the name of the entity."""
|
"""Return the name of the entity."""
|
||||||
|
@ -122,3 +131,12 @@ class AlarmDecoderBinarySensor(BinarySensorDevice):
|
||||||
if self._rfid and message and message.serial_number == self._rfid:
|
if self._rfid and message and message.serial_number == self._rfid:
|
||||||
self._rfstate = message.value
|
self._rfstate = message.value
|
||||||
self.schedule_update_ha_state()
|
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()
|
self.arest.update()
|
||||||
|
|
||||||
|
|
||||||
class ArestData(object):
|
class ArestData:
|
||||||
"""Class for handling the data retrieval for pins."""
|
"""Class for handling the data retrieval for pins."""
|
||||||
|
|
||||||
def __init__(self, resource, pin):
|
def __init__(self, resource, pin):
|
||||||
|
|
|
@ -99,7 +99,7 @@ class AuroraSensor(BinarySensorDevice):
|
||||||
self.aurora_data.update()
|
self.aurora_data.update()
|
||||||
|
|
||||||
|
|
||||||
class AuroraData(object):
|
class AuroraData:
|
||||||
"""Get aurora forecast."""
|
"""Get aurora forecast."""
|
||||||
|
|
||||||
def __init__(self, latitude, longitude, threshold):
|
def __init__(self, latitude, longitude, threshold):
|
||||||
|
|
|
@ -8,7 +8,7 @@ import logging
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
import homeassistant.components.bbb_gpio as bbb_gpio
|
from homeassistant.components import bbb_gpio
|
||||||
from homeassistant.components.binary_sensor import (
|
from homeassistant.components.binary_sensor import (
|
||||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||||
from homeassistant.const import (DEVICE_DEFAULT_NAME, CONF_NAME)
|
from homeassistant.const import (DEVICE_DEFAULT_NAME, CONF_NAME)
|
||||||
|
|
|
@ -25,6 +25,9 @@ DEFAULT_PAYLOAD_OFF = 'OFF'
|
||||||
|
|
||||||
SCAN_INTERVAL = timedelta(seconds=60)
|
SCAN_INTERVAL = timedelta(seconds=60)
|
||||||
|
|
||||||
|
CONF_COMMAND_TIMEOUT = 'command_timeout'
|
||||||
|
DEFAULT_TIMEOUT = 15
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
vol.Required(CONF_COMMAND): cv.string,
|
vol.Required(CONF_COMMAND): cv.string,
|
||||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): 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_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string,
|
||||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||||
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
|
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)
|
payload_on = config.get(CONF_PAYLOAD_ON)
|
||||||
device_class = config.get(CONF_DEVICE_CLASS)
|
device_class = config.get(CONF_DEVICE_CLASS)
|
||||||
value_template = config.get(CONF_VALUE_TEMPLATE)
|
value_template = config.get(CONF_VALUE_TEMPLATE)
|
||||||
|
command_timeout = config.get(CONF_COMMAND_TIMEOUT)
|
||||||
if value_template is not None:
|
if value_template is not None:
|
||||||
value_template.hass = hass
|
value_template.hass = hass
|
||||||
data = CommandSensorData(hass, command)
|
data = CommandSensorData(hass, command, command_timeout)
|
||||||
|
|
||||||
add_devices([CommandBinarySensor(
|
add_devices([CommandBinarySensor(
|
||||||
hass, data, name, device_class, payload_on, payload_off,
|
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)
|
add_entities(entities)
|
||||||
|
|
||||||
|
|
||||||
class HikvisionData(object):
|
class HikvisionData:
|
||||||
"""Hikvision device event stream object."""
|
"""Hikvision device event stream object."""
|
||||||
|
|
||||||
def __init__(self, hass, url, port, name, username, password):
|
def __init__(self, hass, url, port, name, username, password):
|
||||||
|
|
|
@ -3,8 +3,6 @@
|
||||||
For more details about this platform, please refer to the documentation at
|
For more details about this platform, please refer to the documentation at
|
||||||
https://home-assistant.io/components/binary_sensor.ihc/
|
https://home-assistant.io/components/binary_sensor.ihc/
|
||||||
"""
|
"""
|
||||||
from xml.etree.ElementTree import Element
|
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import (
|
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,
|
def __init__(self, ihc_controller, name, ihc_id: int, info: bool,
|
||||||
sensor_type: str, inverting: bool,
|
sensor_type: str, inverting: bool,
|
||||||
product: Element = None) -> None:
|
product=None) -> None:
|
||||||
"""Initialize the IHC binary sensor."""
|
"""Initialize the IHC binary sensor."""
|
||||||
super().__init__(ihc_controller, name, ihc_id, info, product)
|
super().__init__(ihc_controller, name, ihc_id, info, product)
|
||||||
self._state = None
|
self._state = None
|
||||||
|
|
|
@ -17,7 +17,9 @@ _LOGGER = logging.getLogger(__name__)
|
||||||
SENSOR_TYPES = {'openClosedSensor': 'opening',
|
SENSOR_TYPES = {'openClosedSensor': 'opening',
|
||||||
'motionSensor': 'motion',
|
'motionSensor': 'motion',
|
||||||
'doorSensor': 'door',
|
'doorSensor': 'door',
|
||||||
'wetLeakSensor': 'moisture'}
|
'wetLeakSensor': 'moisture',
|
||||||
|
'lightSensor': 'light',
|
||||||
|
'batterySensor': 'battery'}
|
||||||
|
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
|
@ -54,4 +56,9 @@ class InsteonPLMBinarySensor(InsteonPLMEntity, BinarySensorDevice):
|
||||||
@property
|
@property
|
||||||
def is_on(self):
|
def is_on(self):
|
||||||
"""Return the boolean response if the node is on."""
|
"""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()
|
self.iss_data.update()
|
||||||
|
|
||||||
|
|
||||||
class IssData(object):
|
class IssData:
|
||||||
"""Get data from the ISS API."""
|
"""Get data from the ISS API."""
|
||||||
|
|
||||||
def __init__(self, latitude, longitude):
|
def __init__(self, latitude, longitude):
|
||||||
|
|
|
@ -55,7 +55,7 @@ def setup_platform(hass, config: ConfigType,
|
||||||
else:
|
else:
|
||||||
device_type = _detect_device_type(node)
|
device_type = _detect_device_type(node)
|
||||||
subnode_id = int(node.nid[-1])
|
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
|
# These sensors use an optional "negative" subnode 2 to snag
|
||||||
# all state changes
|
# all state changes
|
||||||
if subnode_id == 2:
|
if subnode_id == 2:
|
||||||
|
|
|
@ -7,7 +7,7 @@ https://home-assistant.io/components/binary_sensor.modbus/
|
||||||
import logging
|
import logging
|
||||||
import voluptuous as vol
|
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.const import CONF_NAME, CONF_SLAVE
|
||||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
|
|
|
@ -11,7 +11,7 @@ from typing import Optional
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
import homeassistant.components.mqtt as mqtt
|
from homeassistant.components import mqtt
|
||||||
from homeassistant.components.binary_sensor import (
|
from homeassistant.components.binary_sensor import (
|
||||||
BinarySensorDevice, DEVICE_CLASSES_SCHEMA)
|
BinarySensorDevice, DEVICE_CLASSES_SCHEMA)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
|
|
|
@ -142,7 +142,7 @@ class NetatmoBinarySensor(BinarySensorDevice):
|
||||||
"""Return the class of this sensor, from DEVICE_CLASSES."""
|
"""Return the class of this sensor, from DEVICE_CLASSES."""
|
||||||
if self._cameratype == 'NACamera':
|
if self._cameratype == 'NACamera':
|
||||||
return WELCOME_SENSOR_TYPES.get(self._sensor_name)
|
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 PRESENCE_SENSOR_TYPES.get(self._sensor_name)
|
||||||
return TAG_SENSOR_TYPES.get(self._sensor_name)
|
return TAG_SENSOR_TYPES.get(self._sensor_name)
|
||||||
|
|
||||||
|
|
|
@ -96,7 +96,7 @@ class PingBinarySensor(BinarySensorDevice):
|
||||||
self.ping.update()
|
self.ping.update()
|
||||||
|
|
||||||
|
|
||||||
class PingData(object):
|
class PingData:
|
||||||
"""The Class for handling the data retrieval."""
|
"""The Class for handling the data retrieval."""
|
||||||
|
|
||||||
def __init__(self, host, count):
|
def __init__(self, host, count):
|
||||||
|
|
|
@ -111,11 +111,10 @@ class RachioControllerOnlineBinarySensor(RachioControllerBinarySensor):
|
||||||
|
|
||||||
if data[KEY_STATUS] == STATUS_ONLINE:
|
if data[KEY_STATUS] == STATUS_ONLINE:
|
||||||
return True
|
return True
|
||||||
elif data[KEY_STATUS] == STATUS_OFFLINE:
|
if data[KEY_STATUS] == STATUS_OFFLINE:
|
||||||
return False
|
return False
|
||||||
else:
|
_LOGGER.warning('"%s" reported in unknown state "%s"', self.name,
|
||||||
_LOGGER.warning('"%s" reported in unknown state "%s"', self.name,
|
data[KEY_STATUS])
|
||||||
data[KEY_STATUS])
|
|
||||||
|
|
||||||
def _handle_update(self, *args, **kwargs) -> None:
|
def _handle_update(self, *args, **kwargs) -> None:
|
||||||
"""Handle an update to the state of this sensor."""
|
"""Handle an update to the state of this sensor."""
|
||||||
|
|
|
@ -67,6 +67,6 @@ class RainCloudBinarySensor(RainCloudEntity, BinarySensorDevice):
|
||||||
"""Return the icon of this device."""
|
"""Return the icon of this device."""
|
||||||
if self._sensor_type == 'is_watering':
|
if self._sensor_type == 'is_watering':
|
||||||
return 'mdi:water' if self.is_on else 'mdi:water-off'
|
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 'mdi:pipe' if self.is_on else 'mdi:pipe-disconnected'
|
||||||
return ICON_MAP.get(self._sensor_type)
|
return ICON_MAP.get(self._sensor_type)
|
||||||
|
|
|
@ -8,7 +8,7 @@ import logging
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
import homeassistant.components.rpi_gpio as rpi_gpio
|
from homeassistant.components import rpi_gpio
|
||||||
from homeassistant.components.binary_sensor import (
|
from homeassistant.components.binary_sensor import (
|
||||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||||
from homeassistant.const import DEVICE_DEFAULT_NAME
|
from homeassistant.const import DEVICE_DEFAULT_NAME
|
||||||
|
|
|
@ -10,7 +10,7 @@ import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import (
|
from homeassistant.components.binary_sensor import (
|
||||||
PLATFORM_SCHEMA, BinarySensorDevice)
|
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
|
from homeassistant.const import CONF_NAME, DEVICE_DEFAULT_NAME
|
||||||
import homeassistant.helpers.config_validation as cv
|
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()
|
self.data.update()
|
||||||
|
|
||||||
|
|
||||||
class TapsAffData(object):
|
class TapsAffData:
|
||||||
"""Class for handling the data retrieval for pins."""
|
"""Class for handling the data retrieval for pins."""
|
||||||
|
|
||||||
def __init__(self, location):
|
def __init__(self, location):
|
||||||
|
|
|
@ -129,9 +129,9 @@ class ThresholdSensor(BinarySensorDevice):
|
||||||
if self._threshold_lower is not None and \
|
if self._threshold_lower is not None and \
|
||||||
self._threshold_upper is not None:
|
self._threshold_upper is not None:
|
||||||
return TYPE_RANGE
|
return TYPE_RANGE
|
||||||
elif self._threshold_lower is not None:
|
if self._threshold_lower is not None:
|
||||||
return TYPE_LOWER
|
return TYPE_LOWER
|
||||||
elif self._threshold_upper is not None:
|
if self._threshold_upper is not None:
|
||||||
return TYPE_UPPER
|
return TYPE_UPPER
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
|
@ -23,7 +23,7 @@ from homeassistant.helpers.entity import generate_entity_id
|
||||||
from homeassistant.helpers.event import async_track_state_change
|
from homeassistant.helpers.event import async_track_state_change
|
||||||
from homeassistant.util import utcnow
|
from homeassistant.util import utcnow
|
||||||
|
|
||||||
REQUIREMENTS = ['numpy==1.14.5']
|
REQUIREMENTS = ['numpy==1.15.0']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,7 @@ class VolvoSensor(VolvoEntity, BinarySensorDevice):
|
||||||
val = getattr(self.vehicle, self._attribute)
|
val = getattr(self.vehicle, self._attribute)
|
||||||
if self._attribute == 'bulb_failures':
|
if self._attribute == 'bulb_failures':
|
||||||
return bool(val)
|
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 any([val[key] for key in val if 'Open' in key])
|
||||||
return val != 'Normal'
|
return val != 'Normal'
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||||
"""Register discovered WeMo binary sensors."""
|
"""Register discovered WeMo binary sensors."""
|
||||||
import pywemo.discovery as discovery
|
from pywemo import discovery
|
||||||
|
|
||||||
if discovery_info is not None:
|
if discovery_info is not None:
|
||||||
location = discovery_info['ssdp_description']
|
location = discovery_info['ssdp_description']
|
||||||
|
|
|
@ -135,7 +135,7 @@ class IsWorkdaySensor(BinarySensorDevice):
|
||||||
"""Check if given day is in the includes list."""
|
"""Check if given day is in the includes list."""
|
||||||
if day in self._workdays:
|
if day in self._workdays:
|
||||||
return True
|
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 True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
@ -144,7 +144,7 @@ class IsWorkdaySensor(BinarySensorDevice):
|
||||||
"""Check if given day is in the excludes list."""
|
"""Check if given day is in the excludes list."""
|
||||||
if day in self._excludes:
|
if day in self._excludes:
|
||||||
return True
|
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 True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
|
@ -124,7 +124,7 @@ class XiaomiNatgasSensor(XiaomiBinarySensor):
|
||||||
return False
|
return False
|
||||||
self._state = True
|
self._state = True
|
||||||
return True
|
return True
|
||||||
elif value == '0':
|
if value == '0':
|
||||||
if self._state:
|
if self._state:
|
||||||
self._state = False
|
self._state = False
|
||||||
return True
|
return True
|
||||||
|
@ -184,7 +184,7 @@ class XiaomiMotionSensor(XiaomiBinarySensor):
|
||||||
return False
|
return False
|
||||||
self._state = True
|
self._state = True
|
||||||
return True
|
return True
|
||||||
elif value == NO_MOTION:
|
if value == NO_MOTION:
|
||||||
if not self._state:
|
if not self._state:
|
||||||
return False
|
return False
|
||||||
self._state = False
|
self._state = False
|
||||||
|
@ -224,7 +224,7 @@ class XiaomiDoorSensor(XiaomiBinarySensor):
|
||||||
return False
|
return False
|
||||||
self._state = True
|
self._state = True
|
||||||
return True
|
return True
|
||||||
elif value == 'close':
|
if value == 'close':
|
||||||
self._open_since = 0
|
self._open_since = 0
|
||||||
if self._state:
|
if self._state:
|
||||||
self._state = False
|
self._state = False
|
||||||
|
@ -254,7 +254,7 @@ class XiaomiWaterLeakSensor(XiaomiBinarySensor):
|
||||||
return False
|
return False
|
||||||
self._state = True
|
self._state = True
|
||||||
return True
|
return True
|
||||||
elif value == 'no_leak':
|
if value == 'no_leak':
|
||||||
if self._state:
|
if self._state:
|
||||||
self._state = False
|
self._state = False
|
||||||
return True
|
return True
|
||||||
|
@ -290,7 +290,7 @@ class XiaomiSmokeSensor(XiaomiBinarySensor):
|
||||||
return False
|
return False
|
||||||
self._state = True
|
self._state = True
|
||||||
return True
|
return True
|
||||||
elif value == '0':
|
if value == '0':
|
||||||
if self._state:
|
if self._state:
|
||||||
self._state = False
|
self._state = False
|
||||||
return True
|
return True
|
||||||
|
|
|
@ -10,7 +10,7 @@ import homeassistant.util.dt as dt_util
|
||||||
from homeassistant.helpers.event import track_point_in_time
|
from homeassistant.helpers.event import track_point_in_time
|
||||||
from homeassistant.components import zwave
|
from homeassistant.components import zwave
|
||||||
from homeassistant.components.zwave import workaround
|
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 (
|
from homeassistant.components.binary_sensor import (
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
BinarySensorDevice)
|
BinarySensorDevice)
|
||||||
|
|
|
@ -40,7 +40,7 @@ SNAP_PICTURE_SCHEMA = vol.Schema({
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class BlinkSystem(object):
|
class BlinkSystem:
|
||||||
"""Blink System class."""
|
"""Blink System class."""
|
||||||
|
|
||||||
def __init__(self, config_info):
|
def __init__(self, config_info):
|
||||||
|
|
|
@ -50,7 +50,7 @@ def setup(hass, config):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class BloomSky(object):
|
class BloomSky:
|
||||||
"""Handle all communication with the BloomSky API."""
|
"""Handle all communication with the BloomSky API."""
|
||||||
|
|
||||||
# API documentation at http://weatherlution.com/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
|
return cd_account
|
||||||
|
|
||||||
|
|
||||||
class BMWConnectedDriveAccount(object):
|
class BMWConnectedDriveAccount:
|
||||||
"""Representation of a BMW vehicle."""
|
"""Representation of a BMW vehicle."""
|
||||||
|
|
||||||
def __init__(self, username: str, password: str, region_str: str,
|
def __init__(self, username: str, password: str, region_str: str,
|
||||||
|
|
|
@ -130,7 +130,7 @@ class CalendarEventDevice(Entity):
|
||||||
|
|
||||||
now = dt.now()
|
now = dt.now()
|
||||||
|
|
||||||
if start <= now and end > now:
|
if start <= now < end:
|
||||||
return STATE_ON
|
return STATE_ON
|
||||||
|
|
||||||
if now >= end:
|
if now >= end:
|
||||||
|
|
|
@ -125,7 +125,7 @@ class WebDavCalendarEventDevice(CalendarEventDevice):
|
||||||
return await self.data.async_get_events(hass, start_date, end_date)
|
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."""
|
"""Class to utilize the calendar dav client object to get next event."""
|
||||||
|
|
||||||
def __init__(self, calendar, include_all_day, search):
|
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."""
|
"""Representation of a Demo Calendar element."""
|
||||||
|
|
||||||
event = {}
|
event = {}
|
||||||
|
|
|
@ -55,7 +55,7 @@ class GoogleCalendarEventDevice(CalendarEventDevice):
|
||||||
return await self.data.async_get_events(hass, start_date, end_date)
|
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."""
|
"""Class to utilize calendar service object to get next event."""
|
||||||
|
|
||||||
def __init__(self, calendar_service, calendar_id, search,
|
def __init__(self, calendar_service, calendar_id, search,
|
||||||
|
|
|
@ -26,6 +26,9 @@ CONF_PROJECT_DUE_DATE = 'due_date_days'
|
||||||
CONF_PROJECT_LABEL_WHITELIST = 'labels'
|
CONF_PROJECT_LABEL_WHITELIST = 'labels'
|
||||||
CONF_PROJECT_WHITELIST = 'include_projects'
|
CONF_PROJECT_WHITELIST = 'include_projects'
|
||||||
|
|
||||||
|
# https://github.com/PyCQA/pylint/pull/2320
|
||||||
|
# pylint: disable=fixme
|
||||||
|
|
||||||
# Calendar Platform: Does this calendar event last all day?
|
# Calendar Platform: Does this calendar event last all day?
|
||||||
ALL_DAY = 'all_day'
|
ALL_DAY = 'all_day'
|
||||||
# Attribute: All tasks in this project
|
# Attribute: All tasks in this project
|
||||||
|
@ -280,7 +283,7 @@ class TodoistProjectDevice(CalendarEventDevice):
|
||||||
return attributes
|
return attributes
|
||||||
|
|
||||||
|
|
||||||
class TodoistProjectData(object):
|
class TodoistProjectData:
|
||||||
"""
|
"""
|
||||||
Class used by the Task Device service object to hold all Todoist Tasks.
|
Class used by the Task Device service object to hold all Todoist Tasks.
|
||||||
|
|
||||||
|
@ -503,7 +506,7 @@ class TodoistProjectData(object):
|
||||||
time_format = '%a %d %b %Y %H:%M:%S %z'
|
time_format = '%a %d %b %Y %H:%M:%S %z'
|
||||||
for task in project_task_data:
|
for task in project_task_data:
|
||||||
due_date = datetime.strptime(task['due_date_utc'], time_format)
|
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 = {
|
event = {
|
||||||
'uid': task['id'],
|
'uid': task['id'],
|
||||||
'title': task['content'],
|
'title': task['content'],
|
||||||
|
|
|
@ -19,7 +19,8 @@ import async_timeout
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.core import callback
|
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.exceptions import HomeAssistantError
|
||||||
from homeassistant.loader import bind_hass
|
from homeassistant.loader import bind_hass
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
|
@ -47,6 +48,9 @@ STATE_RECORDING = 'recording'
|
||||||
STATE_STREAMING = 'streaming'
|
STATE_STREAMING = 'streaming'
|
||||||
STATE_IDLE = 'idle'
|
STATE_IDLE = 'idle'
|
||||||
|
|
||||||
|
# Bitfield of features supported by the camera entity
|
||||||
|
SUPPORT_ON_OFF = 1
|
||||||
|
|
||||||
DEFAULT_CONTENT_TYPE = 'image/jpeg'
|
DEFAULT_CONTENT_TYPE = 'image/jpeg'
|
||||||
ENTITY_IMAGE_URL = '/api/camera_proxy/{0}?token={1}'
|
ENTITY_IMAGE_URL = '/api/camera_proxy/{0}?token={1}'
|
||||||
|
|
||||||
|
@ -79,6 +83,35 @@ class Image:
|
||||||
content = attr.ib(type=bytes)
|
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
|
@bind_hass
|
||||||
def enable_motion_detection(hass, entity_id=None):
|
def enable_motion_detection(hass, entity_id=None):
|
||||||
"""Enable Motion Detection."""
|
"""Enable Motion Detection."""
|
||||||
|
@ -119,6 +152,9 @@ async def async_get_image(hass, entity_id, timeout=10):
|
||||||
if camera is None:
|
if camera is None:
|
||||||
raise HomeAssistantError('Camera not found')
|
raise HomeAssistantError('Camera not found')
|
||||||
|
|
||||||
|
if not camera.is_on:
|
||||||
|
raise HomeAssistantError('Camera is off')
|
||||||
|
|
||||||
with suppress(asyncio.CancelledError, asyncio.TimeoutError):
|
with suppress(asyncio.CancelledError, asyncio.TimeoutError):
|
||||||
with async_timeout.timeout(timeout, loop=hass.loop):
|
with async_timeout.timeout(timeout, loop=hass.loop):
|
||||||
image = await camera.async_camera_image()
|
image = await camera.async_camera_image()
|
||||||
|
@ -163,6 +199,12 @@ async def async_setup(hass, config):
|
||||||
await camera.async_enable_motion_detection()
|
await camera.async_enable_motion_detection()
|
||||||
elif service.service == SERVICE_DISABLE_MOTION:
|
elif service.service == SERVICE_DISABLE_MOTION:
|
||||||
await camera.async_disable_motion_detection()
|
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:
|
if not camera.should_poll:
|
||||||
continue
|
continue
|
||||||
|
@ -200,6 +242,12 @@ async def async_setup(hass, config):
|
||||||
except OSError as err:
|
except OSError as err:
|
||||||
_LOGGER.error("Can't write image to file: %s", 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(
|
hass.services.async_register(
|
||||||
DOMAIN, SERVICE_ENABLE_MOTION, async_handle_camera_service,
|
DOMAIN, SERVICE_ENABLE_MOTION, async_handle_camera_service,
|
||||||
schema=CAMERA_SERVICE_SCHEMA)
|
schema=CAMERA_SERVICE_SCHEMA)
|
||||||
|
@ -243,6 +291,11 @@ class Camera(Entity):
|
||||||
"""Return a link to the camera feed as entity picture."""
|
"""Return a link to the camera feed as entity picture."""
|
||||||
return ENTITY_IMAGE_URL.format(self.entity_id, self.access_tokens[-1])
|
return ENTITY_IMAGE_URL.format(self.entity_id, self.access_tokens[-1])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported_features(self):
|
||||||
|
"""Flag supported features."""
|
||||||
|
return 0
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_recording(self):
|
def is_recording(self):
|
||||||
"""Return true if the device is recording."""
|
"""Return true if the device is recording."""
|
||||||
|
@ -301,32 +354,23 @@ class Camera(Entity):
|
||||||
|
|
||||||
last_image = None
|
last_image = None
|
||||||
|
|
||||||
try:
|
while True:
|
||||||
while True:
|
img_bytes = await self.async_camera_image()
|
||||||
img_bytes = await self.async_camera_image()
|
if not img_bytes:
|
||||||
if not img_bytes:
|
break
|
||||||
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)
|
await write_to_mjpeg_stream(img_bytes)
|
||||||
|
last_image = img_bytes
|
||||||
|
|
||||||
# Chrome seems to always ignore first picture,
|
await asyncio.sleep(interval)
|
||||||
# print it twice.
|
|
||||||
if last_image is None:
|
|
||||||
await write_to_mjpeg_stream(img_bytes)
|
|
||||||
|
|
||||||
last_image = img_bytes
|
return response
|
||||||
|
|
||||||
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()
|
|
||||||
|
|
||||||
async def handle_async_mjpeg_stream(self, request):
|
async def handle_async_mjpeg_stream(self, request):
|
||||||
"""Serve an HTTP MJPEG stream from the camera.
|
"""Serve an HTTP MJPEG stream from the camera.
|
||||||
|
@ -342,14 +386,38 @@ class Camera(Entity):
|
||||||
"""Return the camera state."""
|
"""Return the camera state."""
|
||||||
if self.is_recording:
|
if self.is_recording:
|
||||||
return STATE_RECORDING
|
return STATE_RECORDING
|
||||||
elif self.is_streaming:
|
if self.is_streaming:
|
||||||
return STATE_STREAMING
|
return STATE_STREAMING
|
||||||
return STATE_IDLE
|
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):
|
def enable_motion_detection(self):
|
||||||
"""Enable motion detection in the camera."""
|
"""Enable motion detection in the camera."""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@callback
|
||||||
def async_enable_motion_detection(self):
|
def async_enable_motion_detection(self):
|
||||||
"""Call the job and enable motion detection."""
|
"""Call the job and enable motion detection."""
|
||||||
return self.hass.async_add_job(self.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."""
|
"""Disable motion detection in camera."""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@callback
|
||||||
def async_disable_motion_detection(self):
|
def async_disable_motion_detection(self):
|
||||||
"""Call the job and disable motion detection."""
|
"""Call the job and disable motion detection."""
|
||||||
return self.hass.async_add_job(self.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)
|
camera = self.component.get_entity(entity_id)
|
||||||
|
|
||||||
if camera is None:
|
if camera is None:
|
||||||
status = 404 if request[KEY_AUTHENTICATED] else 401
|
raise web.HTTPNotFound()
|
||||||
return web.Response(status=status)
|
|
||||||
|
|
||||||
authenticated = (request[KEY_AUTHENTICATED] or
|
authenticated = (request[KEY_AUTHENTICATED] or
|
||||||
request.query.get('token') in camera.access_tokens)
|
request.query.get('token') in camera.access_tokens)
|
||||||
|
|
||||||
if not authenticated:
|
if not authenticated:
|
||||||
return web.Response(status=401)
|
raise web.HTTPUnauthorized()
|
||||||
|
|
||||||
response = await self.handle(request, camera)
|
if not camera.is_on:
|
||||||
return response
|
_LOGGER.debug('Camera is off.')
|
||||||
|
raise web.HTTPServiceUnavailable()
|
||||||
|
|
||||||
|
return await self.handle(request, camera)
|
||||||
|
|
||||||
async def handle(self, request, camera):
|
async def handle(self, request, camera):
|
||||||
"""Handle the camera request."""
|
"""Handle the camera request."""
|
||||||
|
@ -435,7 +506,7 @@ class CameraImageView(CameraView):
|
||||||
return web.Response(body=image,
|
return web.Response(body=image,
|
||||||
content_type=camera.content_type)
|
content_type=camera.content_type)
|
||||||
|
|
||||||
return web.Response(status=500)
|
raise web.HTTPInternalServerError()
|
||||||
|
|
||||||
|
|
||||||
class CameraMjpegStream(CameraView):
|
class CameraMjpegStream(CameraView):
|
||||||
|
@ -448,8 +519,7 @@ class CameraMjpegStream(CameraView):
|
||||||
"""Serve camera stream, possibly with interval."""
|
"""Serve camera stream, possibly with interval."""
|
||||||
interval = request.query.get('interval')
|
interval = request.query.get('interval')
|
||||||
if interval is None:
|
if interval is None:
|
||||||
await camera.handle_async_mjpeg_stream(request)
|
return await camera.handle_async_mjpeg_stream(request)
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Compose camera stream from stills
|
# Compose camera stream from stills
|
||||||
|
@ -457,10 +527,9 @@ class CameraMjpegStream(CameraView):
|
||||||
if interval < MIN_STREAM_INTERVAL:
|
if interval < MIN_STREAM_INTERVAL:
|
||||||
raise ValueError("Stream interval must be be > {}"
|
raise ValueError("Stream interval must be be > {}"
|
||||||
.format(MIN_STREAM_INTERVAL))
|
.format(MIN_STREAM_INTERVAL))
|
||||||
await camera.handle_async_still_stream(request, interval)
|
return await camera.handle_async_still_stream(request, interval)
|
||||||
return
|
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return web.Response(status=400)
|
raise web.HTTPBadRequest()
|
||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
|
|
|
@ -64,7 +64,7 @@ class AmcrestCam(Camera):
|
||||||
yield from super().handle_async_mjpeg_stream(request)
|
yield from super().handle_async_mjpeg_stream(request)
|
||||||
return
|
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
|
# stream an MJPEG image stream directly from the camera
|
||||||
websession = async_get_clientsession(self.hass)
|
websession = async_get_clientsession(self.hass)
|
||||||
streaming_url = self._camera.mjpeg_url(typeno=self._resolution)
|
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."""
|
"""Set the URL to get the image."""
|
||||||
if mode == 'mjpeg':
|
if mode == 'mjpeg':
|
||||||
return 'http://{}:{}/axis-cgi/mjpg/video.cgi'.format(host, port)
|
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)
|
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
|
For more details about this platform, please refer to the documentation
|
||||||
https://home-assistant.io/components/demo/
|
https://home-assistant.io/components/demo/
|
||||||
"""
|
"""
|
||||||
import os
|
|
||||||
import logging
|
import logging
|
||||||
import homeassistant.util.dt as dt_util
|
import os
|
||||||
from homeassistant.components.camera import Camera
|
|
||||||
|
from homeassistant.components.camera import Camera, SUPPORT_ON_OFF
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -16,26 +16,29 @@ async def async_setup_platform(hass, config, async_add_devices,
|
||||||
discovery_info=None):
|
discovery_info=None):
|
||||||
"""Set up the Demo camera platform."""
|
"""Set up the Demo camera platform."""
|
||||||
async_add_devices([
|
async_add_devices([
|
||||||
DemoCamera(hass, config, 'Demo camera')
|
DemoCamera('Demo camera')
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
class DemoCamera(Camera):
|
class DemoCamera(Camera):
|
||||||
"""The representation of a Demo camera."""
|
"""The representation of a Demo camera."""
|
||||||
|
|
||||||
def __init__(self, hass, config, name):
|
def __init__(self, name):
|
||||||
"""Initialize demo camera component."""
|
"""Initialize demo camera component."""
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._parent = hass
|
|
||||||
self._name = name
|
self._name = name
|
||||||
self._motion_status = False
|
self._motion_status = False
|
||||||
|
self.is_streaming = True
|
||||||
|
self._images_index = 0
|
||||||
|
|
||||||
def camera_image(self):
|
def camera_image(self):
|
||||||
"""Return a faked still image response."""
|
"""Return a faked still image response."""
|
||||||
now = dt_util.utcnow()
|
self._images_index = (self._images_index + 1) % 4
|
||||||
|
|
||||||
image_path = os.path.join(
|
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:
|
with open(image_path, 'rb') as file:
|
||||||
return file.read()
|
return file.read()
|
||||||
|
|
||||||
|
@ -46,8 +49,21 @@ class DemoCamera(Camera):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def should_poll(self):
|
def should_poll(self):
|
||||||
"""Camera should poll periodically."""
|
"""Demo camera doesn't need poll.
|
||||||
return True
|
|
||||||
|
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
|
@property
|
||||||
def motion_detection_enabled(self):
|
def motion_detection_enabled(self):
|
||||||
|
@ -57,7 +73,19 @@ class DemoCamera(Camera):
|
||||||
def enable_motion_detection(self):
|
def enable_motion_detection(self):
|
||||||
"""Enable the Motion detection in base station (Arm)."""
|
"""Enable the Motion detection in base station (Arm)."""
|
||||||
self._motion_status = True
|
self._motion_status = True
|
||||||
|
self.schedule_update_ha_state()
|
||||||
|
|
||||||
def disable_motion_detection(self):
|
def disable_motion_detection(self):
|
||||||
"""Disable the motion detection in base station (Disarm)."""
|
"""Disable the motion detection in base station (Disarm)."""
|
||||||
self._motion_status = False
|
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
|
async def async_setup_platform(hass, config, async_add_devices,
|
||||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
discovery_info=None):
|
||||||
"""Set up a FFmpeg camera."""
|
"""Set up a FFmpeg camera."""
|
||||||
if not hass.data[DATA_FFMPEG].async_run_test(config.get(CONF_INPUT)):
|
if not hass.data[DATA_FFMPEG].async_run_test(config.get(CONF_INPUT)):
|
||||||
return
|
return
|
||||||
|
@ -49,30 +49,30 @@ class FFmpegCamera(Camera):
|
||||||
self._input = config.get(CONF_INPUT)
|
self._input = config.get(CONF_INPUT)
|
||||||
self._extra_arguments = config.get(CONF_EXTRA_ARGUMENTS)
|
self._extra_arguments = config.get(CONF_EXTRA_ARGUMENTS)
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def async_camera_image(self):
|
||||||
def async_camera_image(self):
|
|
||||||
"""Return a still image response from the camera."""
|
"""Return a still image response from the camera."""
|
||||||
from haffmpeg import ImageFrame, IMAGE_JPEG
|
from haffmpeg import ImageFrame, IMAGE_JPEG
|
||||||
ffmpeg = ImageFrame(self._manager.binary, loop=self.hass.loop)
|
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,
|
self._input, output_format=IMAGE_JPEG,
|
||||||
extra_cmd=self._extra_arguments), loop=self.hass.loop)
|
extra_cmd=self._extra_arguments), loop=self.hass.loop)
|
||||||
return image
|
return image
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def handle_async_mjpeg_stream(self, request):
|
||||||
def handle_async_mjpeg_stream(self, request):
|
|
||||||
"""Generate an HTTP MJPEG stream from the camera."""
|
"""Generate an HTTP MJPEG stream from the camera."""
|
||||||
from haffmpeg import CameraMjpeg
|
from haffmpeg import CameraMjpeg
|
||||||
|
|
||||||
stream = CameraMjpeg(self._manager.binary, loop=self.hass.loop)
|
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)
|
self._input, extra_cmd=self._extra_arguments)
|
||||||
|
|
||||||
yield from async_aiohttp_proxy_stream(
|
try:
|
||||||
self.hass, request, stream,
|
return await async_aiohttp_proxy_stream(
|
||||||
'multipart/x-mixed-replace;boundary=ffserver')
|
self.hass, request, stream,
|
||||||
yield from stream.close()
|
'multipart/x-mixed-replace;boundary=ffserver')
|
||||||
|
finally:
|
||||||
|
await stream.close()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
|
|
|
@ -123,19 +123,18 @@ class MjpegCamera(Camera):
|
||||||
with closing(req) as response:
|
with closing(req) as response:
|
||||||
return extract_image_from_mjpeg(response.iter_content(102400))
|
return extract_image_from_mjpeg(response.iter_content(102400))
|
||||||
|
|
||||||
@asyncio.coroutine
|
async def handle_async_mjpeg_stream(self, request):
|
||||||
def handle_async_mjpeg_stream(self, request):
|
|
||||||
"""Generate an HTTP MJPEG stream from the camera."""
|
"""Generate an HTTP MJPEG stream from the camera."""
|
||||||
# aiohttp don't support DigestAuth -> Fallback
|
# aiohttp don't support DigestAuth -> Fallback
|
||||||
if self._authentication == HTTP_DIGEST_AUTHENTICATION:
|
if self._authentication == HTTP_DIGEST_AUTHENTICATION:
|
||||||
yield from super().handle_async_mjpeg_stream(request)
|
await super().handle_async_mjpeg_stream(request)
|
||||||
return
|
return
|
||||||
|
|
||||||
# connect to stream
|
# connect to stream
|
||||||
websession = async_get_clientsession(self.hass)
|
websession = async_get_clientsession(self.hass)
|
||||||
stream_coro = websession.get(self._mjpeg_url, auth=self._auth)
|
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
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
|
|
|
@ -11,7 +11,7 @@ import logging
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
import homeassistant.components.mqtt as mqtt
|
from homeassistant.components import mqtt
|
||||||
from homeassistant.const import CONF_NAME
|
from homeassistant.const import CONF_NAME
|
||||||
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA
|
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
|
|
|
@ -9,8 +9,9 @@ from datetime import timedelta
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
import homeassistant.components.nest as nest
|
from homeassistant.components import nest
|
||||||
from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera)
|
from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera,
|
||||||
|
SUPPORT_ON_OFF)
|
||||||
from homeassistant.util.dt import utcnow
|
from homeassistant.util.dt import utcnow
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
@ -76,7 +77,36 @@ class NestCamera(Camera):
|
||||||
"""Return the brand of the camera."""
|
"""Return the brand of the camera."""
|
||||||
return NEST_BRAND
|
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):
|
def update(self):
|
||||||
"""Cache value from Python-nest."""
|
"""Cache value from Python-nest."""
|
||||||
self._location = self.device.where
|
self._location = self.device.where
|
||||||
|
|
|
@ -105,6 +105,6 @@ class NetatmoCamera(Camera):
|
||||||
"""Return the camera model."""
|
"""Return the camera model."""
|
||||||
if self._cameratype == "NOC":
|
if self._cameratype == "NOC":
|
||||||
return "Presence"
|
return "Presence"
|
||||||
elif self._cameratype == "NACamera":
|
if self._cameratype == "NACamera":
|
||||||
return "Welcome"
|
return "Welcome"
|
||||||
return None
|
return None
|
||||||
|
|
|
@ -2,56 +2,53 @@
|
||||||
Proxy camera platform that enables image processing of camera data.
|
Proxy camera platform that enables image processing of camera data.
|
||||||
|
|
||||||
For more details about this platform, please refer to the documentation
|
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 asyncio
|
||||||
|
import logging
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import async_timeout
|
import async_timeout
|
||||||
|
|
||||||
import voluptuous as vol
|
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
|
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 (
|
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__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
CONF_MAX_IMAGE_WIDTH = "max_image_width"
|
CONF_CACHE_IMAGES = 'cache_images'
|
||||||
CONF_IMAGE_QUALITY = "image_quality"
|
CONF_FORCE_RESIZE = 'force_resize'
|
||||||
CONF_IMAGE_REFRESH_RATE = "image_refresh_rate"
|
CONF_IMAGE_QUALITY = 'image_quality'
|
||||||
CONF_FORCE_RESIZE = "force_resize"
|
CONF_IMAGE_REFRESH_RATE = 'image_refresh_rate'
|
||||||
CONF_MAX_STREAM_WIDTH = "max_stream_width"
|
CONF_MAX_IMAGE_WIDTH = 'max_image_width'
|
||||||
CONF_STREAM_QUALITY = "stream_quality"
|
CONF_MAX_STREAM_WIDTH = 'max_stream_width'
|
||||||
CONF_CACHE_IMAGES = "cache_images"
|
CONF_STREAM_QUALITY = 'stream_quality'
|
||||||
|
|
||||||
DEFAULT_BASENAME = "Camera Proxy"
|
DEFAULT_BASENAME = "Camera Proxy"
|
||||||
DEFAULT_QUALITY = 75
|
DEFAULT_QUALITY = 75
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
vol.Required(CONF_ENTITY_ID): cv.entity_id,
|
vol.Required(CONF_ENTITY_ID): cv.entity_id,
|
||||||
vol.Optional(CONF_NAME): cv.string,
|
vol.Optional(CONF_CACHE_IMAGES, False): cv.boolean,
|
||||||
vol.Optional(CONF_MAX_IMAGE_WIDTH): int,
|
vol.Optional(CONF_FORCE_RESIZE, False): cv.boolean,
|
||||||
vol.Optional(CONF_IMAGE_QUALITY): int,
|
vol.Optional(CONF_IMAGE_QUALITY): int,
|
||||||
vol.Optional(CONF_IMAGE_REFRESH_RATE): float,
|
vol.Optional(CONF_IMAGE_REFRESH_RATE): float,
|
||||||
vol.Optional(CONF_FORCE_RESIZE, False): cv.boolean,
|
vol.Optional(CONF_MAX_IMAGE_WIDTH): int,
|
||||||
vol.Optional(CONF_CACHE_IMAGES, False): cv.boolean,
|
|
||||||
vol.Optional(CONF_MAX_STREAM_WIDTH): int,
|
vol.Optional(CONF_MAX_STREAM_WIDTH): int,
|
||||||
|
vol.Optional(CONF_NAME): cv.string,
|
||||||
vol.Optional(CONF_STREAM_QUALITY): int,
|
vol.Optional(CONF_STREAM_QUALITY): int,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_platform(hass, config, async_add_devices,
|
async def async_setup_platform(
|
||||||
discovery_info=None):
|
hass, config, async_add_devices, discovery_info=None):
|
||||||
"""Set up the Proxy camera platform."""
|
"""Set up the Proxy camera platform."""
|
||||||
async_add_devices([ProxyCamera(hass, config)])
|
async_add_devices([ProxyCamera(hass, config)])
|
||||||
|
|
||||||
|
@ -69,7 +66,7 @@ def _resize_image(image, opts):
|
||||||
|
|
||||||
img = Image.open(io.BytesIO(image))
|
img = Image.open(io.BytesIO(image))
|
||||||
imgfmt = str(img.format)
|
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)
|
_LOGGER.debug("Image is of unsupported type: %s", imgfmt)
|
||||||
return image
|
return image
|
||||||
|
|
||||||
|
@ -77,7 +74,7 @@ def _resize_image(image, opts):
|
||||||
old_size = len(image)
|
old_size = len(image)
|
||||||
if old_width <= new_width:
|
if old_width <= new_width:
|
||||||
if opts.quality is None:
|
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
|
return image
|
||||||
new_width = old_width
|
new_width = old_width
|
||||||
|
|
||||||
|
@ -86,7 +83,7 @@ def _resize_image(image, opts):
|
||||||
|
|
||||||
img = img.resize((new_width, new_height), Image.ANTIALIAS)
|
img = img.resize((new_width, new_height), Image.ANTIALIAS)
|
||||||
imgbuf = io.BytesIO()
|
imgbuf = io.BytesIO()
|
||||||
img.save(imgbuf, "JPEG", optimize=True, quality=quality)
|
img.save(imgbuf, 'JPEG', optimize=True, quality=quality)
|
||||||
newimage = imgbuf.getvalue()
|
newimage = imgbuf.getvalue()
|
||||||
if not opts.force_resize and len(newimage) >= old_size:
|
if not opts.force_resize and len(newimage) >= old_size:
|
||||||
_LOGGER.debug("Using original image(%d bytes) "
|
_LOGGER.debug("Using original image(%d bytes) "
|
||||||
|
@ -94,11 +91,9 @@ def _resize_image(image, opts):
|
||||||
old_size, len(newimage))
|
old_size, len(newimage))
|
||||||
return image
|
return image
|
||||||
|
|
||||||
_LOGGER.debug("Resized image "
|
_LOGGER.debug(
|
||||||
"from (%dx%d - %d bytes) "
|
"Resized image from (%dx%d - %d bytes) to (%dx%d - %d bytes)",
|
||||||
"to (%dx%d - %d bytes)",
|
old_width, old_height, old_size, new_width, new_height, len(newimage))
|
||||||
old_width, old_height, old_size,
|
|
||||||
new_width, new_height, len(newimage))
|
|
||||||
return newimage
|
return newimage
|
||||||
|
|
||||||
|
|
||||||
|
@ -112,7 +107,7 @@ class ImageOpts():
|
||||||
self.force_resize = force_resize
|
self.force_resize = force_resize
|
||||||
|
|
||||||
def __bool__(self):
|
def __bool__(self):
|
||||||
"""Bool evalution rules."""
|
"""Bool evaluation rules."""
|
||||||
return bool(self.max_width or self.quality)
|
return bool(self.max_width or self.quality)
|
||||||
|
|
||||||
|
|
||||||
|
@ -133,8 +128,7 @@ class ProxyCamera(Camera):
|
||||||
config.get(CONF_FORCE_RESIZE))
|
config.get(CONF_FORCE_RESIZE))
|
||||||
|
|
||||||
self._stream_opts = ImageOpts(
|
self._stream_opts = ImageOpts(
|
||||||
config.get(CONF_MAX_STREAM_WIDTH),
|
config.get(CONF_MAX_STREAM_WIDTH), config.get(CONF_STREAM_QUALITY),
|
||||||
config.get(CONF_STREAM_QUALITY),
|
|
||||||
True)
|
True)
|
||||||
|
|
||||||
self._image_refresh_rate = config.get(CONF_IMAGE_REFRESH_RATE)
|
self._image_refresh_rate = config.get(CONF_IMAGE_REFRESH_RATE)
|
||||||
|
@ -145,8 +139,7 @@ class ProxyCamera(Camera):
|
||||||
self._last_image = None
|
self._last_image = None
|
||||||
self._headers = (
|
self._headers = (
|
||||||
{HTTP_HEADER_HA_AUTH: self.hass.config.api.api_password}
|
{HTTP_HEADER_HA_AUTH: self.hass.config.api.api_password}
|
||||||
if self.hass.config.api.api_password is not None
|
if self.hass.config.api.api_password is not None else None)
|
||||||
else None)
|
|
||||||
|
|
||||||
def camera_image(self):
|
def camera_image(self):
|
||||||
"""Return camera image."""
|
"""Return camera image."""
|
||||||
|
@ -191,12 +184,12 @@ class ProxyCamera(Camera):
|
||||||
stream_coro = websession.get(url, headers=self._headers)
|
stream_coro = websession.get(url, headers=self._headers)
|
||||||
|
|
||||||
if not self._stream_opts:
|
if not self._stream_opts:
|
||||||
await async_aiohttp_proxy_web(self.hass, request, stream_coro)
|
return await async_aiohttp_proxy_web(
|
||||||
return
|
self.hass, request, stream_coro)
|
||||||
|
|
||||||
response = aiohttp.web.StreamResponse()
|
response = aiohttp.web.StreamResponse()
|
||||||
response.content_type = ('multipart/x-mixed-replace; '
|
response.content_type = (
|
||||||
'boundary=--frameboundary')
|
'multipart/x-mixed-replace; boundary=--frameboundary')
|
||||||
await response.prepare(request)
|
await response.prepare(request)
|
||||||
|
|
||||||
async def write(img_bytes):
|
async def write(img_bytes):
|
||||||
|
@ -229,15 +222,10 @@ class ProxyCamera(Camera):
|
||||||
_resize_image, image, self._stream_opts)
|
_resize_image, image, self._stream_opts)
|
||||||
await write(image)
|
await write(image)
|
||||||
data = data[jpg_end + 2:]
|
data = data[jpg_end + 2:]
|
||||||
except asyncio.CancelledError:
|
|
||||||
_LOGGER.debug("Stream closed by frontend.")
|
|
||||||
req.close()
|
|
||||||
response = None
|
|
||||||
raise
|
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
if response is not None:
|
req.close()
|
||||||
await response.write_eof()
|
|
||||||
|
return response
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
|
|
|
@ -1,5 +1,19 @@
|
||||||
# Describes the format for available camera services
|
# 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:
|
enable_motion_detection:
|
||||||
description: Enable the motion detection in a camera.
|
description: Enable the motion detection in a camera.
|
||||||
fields:
|
fields:
|
||||||
|
|
|
@ -171,10 +171,9 @@ class UnifiVideoCamera(Camera):
|
||||||
if retry:
|
if retry:
|
||||||
self._login()
|
self._login()
|
||||||
return _get_image(retry=False)
|
return _get_image(retry=False)
|
||||||
else:
|
_LOGGER.error(
|
||||||
_LOGGER.error(
|
"Unable to log into camera, unable to get snapshot")
|
||||||
"Unable to log into camera, unable to get snapshot")
|
raise
|
||||||
raise
|
|
||||||
|
|
||||||
return _get_image()
|
return _get_image()
|
||||||
|
|
||||||
|
|
|
@ -66,8 +66,7 @@ class VerisureSmartcam(Camera):
|
||||||
if not image_ids:
|
if not image_ids:
|
||||||
return
|
return
|
||||||
new_image_id = image_ids[0]
|
new_image_id = image_ids[0]
|
||||||
if (new_image_id == '-1' or
|
if new_image_id in ('-1', self._image_id):
|
||||||
self._image_id == new_image_id):
|
|
||||||
_LOGGER.debug("The image is the same, or loading image_id")
|
_LOGGER.debug("The image is the same, or loading image_id")
|
||||||
return
|
return
|
||||||
_LOGGER.debug("Download new image %s", new_image_id)
|
_LOGGER.debug("Download new image %s", new_image_id)
|
||||||
|
|
|
@ -12,7 +12,7 @@ from homeassistant.const import CONF_NAME
|
||||||
from homeassistant.components.camera.mjpeg import (
|
from homeassistant.components.camera.mjpeg import (
|
||||||
CONF_MJPEG_URL, CONF_STILL_IMAGE_URL, MjpegCamera)
|
CONF_MJPEG_URL, CONF_STILL_IMAGE_URL, MjpegCamera)
|
||||||
|
|
||||||
import homeassistant.components.zoneminder as zoneminder
|
from homeassistant.components import zoneminder
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
|
@ -65,7 +65,7 @@ def setup(hass, config):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class CanaryData(object):
|
class CanaryData:
|
||||||
"""Get the latest data and update the states."""
|
"""Get the latest data and update the states."""
|
||||||
|
|
||||||
def __init__(self, username, password, timeout):
|
def __init__(self, username, password, timeout):
|
||||||
|
|
|
@ -22,7 +22,7 @@ async def async_setup(hass, config):
|
||||||
|
|
||||||
async def async_setup_entry(hass, entry):
|
async def async_setup_entry(hass, entry):
|
||||||
"""Set up Cast from a config entry."""
|
"""Set up Cast from a config entry."""
|
||||||
hass.async_add_job(hass.config_entries.async_forward_entry_setup(
|
hass.async_create_task(hass.config_entries.async_forward_entry_setup(
|
||||||
entry, 'media_player'))
|
entry, 'media_player'))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@ async def _async_has_devices(hass):
|
||||||
"""Return if there are devices that can be discovered."""
|
"""Return if there are devices that can be discovered."""
|
||||||
from pychromecast.discovery import discover_chromecasts
|
from pychromecast.discovery import discover_chromecasts
|
||||||
|
|
||||||
return await hass.async_add_job(discover_chromecasts)
|
return await hass.async_add_executor_job(discover_chromecasts)
|
||||||
|
|
||||||
|
|
||||||
config_entry_flow.register_discovery_flow(
|
config_entry_flow.register_discovery_flow(
|
||||||
|
|
|
@ -145,7 +145,7 @@ class DaikinClimate(ClimateDevice):
|
||||||
if value is None:
|
if value is None:
|
||||||
_LOGGER.error("Invalid value requested for key %s", key)
|
_LOGGER.error("Invalid value requested for key %s", key)
|
||||||
else:
|
else:
|
||||||
if value == "-" or value == "--":
|
if value in ("-", "--"):
|
||||||
value = None
|
value = None
|
||||||
elif cast_to_float:
|
elif cast_to_float:
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -177,7 +177,7 @@ class Thermostat(ClimateDevice):
|
||||||
return None
|
return None
|
||||||
if self.current_operation == STATE_HEAT:
|
if self.current_operation == STATE_HEAT:
|
||||||
return self.thermostat['runtime']['desiredHeat'] / 10.0
|
return self.thermostat['runtime']['desiredHeat'] / 10.0
|
||||||
elif self.current_operation == STATE_COOL:
|
if self.current_operation == STATE_COOL:
|
||||||
return self.thermostat['runtime']['desiredCool'] / 10.0
|
return self.thermostat['runtime']['desiredCool'] / 10.0
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -217,15 +217,15 @@ class Thermostat(ClimateDevice):
|
||||||
return 'away'
|
return 'away'
|
||||||
# A permanent hold from away climate
|
# A permanent hold from away climate
|
||||||
return AWAY_MODE
|
return AWAY_MODE
|
||||||
elif event['holdClimateRef'] != "":
|
if event['holdClimateRef'] != "":
|
||||||
# Any other hold based on climate
|
# Any other hold based on climate
|
||||||
return event['holdClimateRef']
|
return event['holdClimateRef']
|
||||||
# Any hold not based on a climate is a temp hold
|
# Any hold not based on a climate is a temp hold
|
||||||
return TEMPERATURE_HOLD
|
return TEMPERATURE_HOLD
|
||||||
elif event['type'].startswith('auto'):
|
if event['type'].startswith('auto'):
|
||||||
# All auto modes are treated as holds
|
# All auto modes are treated as holds
|
||||||
return event['type'][4:].lower()
|
return event['type'][4:].lower()
|
||||||
elif event['type'] == 'vacation':
|
if event['type'] == 'vacation':
|
||||||
self.vacation = event['name']
|
self.vacation = event['name']
|
||||||
return VACATION_HOLD
|
return VACATION_HOLD
|
||||||
return None
|
return None
|
||||||
|
@ -317,7 +317,7 @@ class Thermostat(ClimateDevice):
|
||||||
if hold == hold_mode:
|
if hold == hold_mode:
|
||||||
# no change, so no action required
|
# no change, so no action required
|
||||||
return
|
return
|
||||||
elif hold_mode == 'None' or hold_mode is None:
|
if hold_mode == 'None' or hold_mode is None:
|
||||||
if hold == VACATION_HOLD:
|
if hold == VACATION_HOLD:
|
||||||
self.data.ecobee.delete_vacation(
|
self.data.ecobee.delete_vacation(
|
||||||
self.thermostat_index, self.vacation)
|
self.thermostat_index, self.vacation)
|
||||||
|
|
|
@ -20,7 +20,7 @@ from homeassistant.const import (
|
||||||
from homeassistant.components.climate import (
|
from homeassistant.components.climate import (
|
||||||
ClimateDevice, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE,
|
ClimateDevice, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE,
|
||||||
SUPPORT_FAN_MODE)
|
SUPPORT_FAN_MODE)
|
||||||
import homeassistant.components.modbus as modbus
|
from homeassistant.components import modbus
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
REQUIREMENTS = ['pyflexit==0.3']
|
REQUIREMENTS = ['pyflexit==0.3']
|
||||||
|
|
|
@ -50,7 +50,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
HeatmiserV3Thermostat(
|
HeatmiserV3Thermostat(
|
||||||
heatmiser, tstat.get(CONF_ID), tstat.get(CONF_NAME), serport)
|
heatmiser, tstat.get(CONF_ID), tstat.get(CONF_NAME), serport)
|
||||||
])
|
])
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
class HeatmiserV3Thermostat(ClimateDevice):
|
class HeatmiserV3Thermostat(ClimateDevice):
|
||||||
|
|
|
@ -87,7 +87,7 @@ class HMThermostat(HMDevice, ClimateDevice):
|
||||||
|
|
||||||
# HM ip etrv 2 uses the set_point_mode to say if its
|
# HM ip etrv 2 uses the set_point_mode to say if its
|
||||||
# auto or manual
|
# auto or manual
|
||||||
elif not set_point_mode == -1:
|
if not set_point_mode == -1:
|
||||||
code = set_point_mode
|
code = set_point_mode
|
||||||
# Other devices use the control_mode
|
# Other devices use the control_mode
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -165,7 +165,7 @@ class RoundThermostat(ClimateDevice):
|
||||||
self.client.set_temperature(self._name, temperature)
|
self.client.set_temperature(self._name, temperature)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_operation(self: ClimateDevice) -> str:
|
def current_operation(self) -> str:
|
||||||
"""Get the current operation of the system."""
|
"""Get the current operation of the system."""
|
||||||
return getattr(self.client, ATTR_SYSTEM_MODE, None)
|
return getattr(self.client, ATTR_SYSTEM_MODE, None)
|
||||||
|
|
||||||
|
@ -174,7 +174,7 @@ class RoundThermostat(ClimateDevice):
|
||||||
"""Return true if away mode is on."""
|
"""Return true if away mode is on."""
|
||||||
return self._away
|
return self._away
|
||||||
|
|
||||||
def set_operation_mode(self: ClimateDevice, operation_mode: str) -> None:
|
def set_operation_mode(self, operation_mode: str) -> None:
|
||||||
"""Set the HVAC mode for the thermostat."""
|
"""Set the HVAC mode for the thermostat."""
|
||||||
if hasattr(self.client, ATTR_SYSTEM_MODE):
|
if hasattr(self.client, ATTR_SYSTEM_MODE):
|
||||||
self.client.system_mode = operation_mode
|
self.client.system_mode = operation_mode
|
||||||
|
@ -280,7 +280,7 @@ class HoneywellUSThermostat(ClimateDevice):
|
||||||
return self._device.setpoint_heat
|
return self._device.setpoint_heat
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_operation(self: ClimateDevice) -> str:
|
def current_operation(self) -> str:
|
||||||
"""Return current operation ie. heat, cool, idle."""
|
"""Return current operation ie. heat, cool, idle."""
|
||||||
oper = getattr(self._device, ATTR_CURRENT_OPERATION, None)
|
oper = getattr(self._device, ATTR_CURRENT_OPERATION, None)
|
||||||
if oper == "off":
|
if oper == "off":
|
||||||
|
@ -373,7 +373,7 @@ class HoneywellUSThermostat(ClimateDevice):
|
||||||
except somecomfort.SomeComfortError:
|
except somecomfort.SomeComfortError:
|
||||||
_LOGGER.error('Can not stop hold mode')
|
_LOGGER.error('Can not stop hold mode')
|
||||||
|
|
||||||
def set_operation_mode(self: ClimateDevice, operation_mode: str) -> None:
|
def set_operation_mode(self, operation_mode: str) -> None:
|
||||||
"""Set the system mode (Cool, Heat, etc)."""
|
"""Set the system mode (Cool, Heat, etc)."""
|
||||||
if hasattr(self._device, ATTR_SYSTEM_MODE):
|
if hasattr(self._device, ATTR_SYSTEM_MODE):
|
||||||
self._device.system_mode = operation_mode
|
self._device.system_mode = operation_mode
|
||||||
|
|
|
@ -192,9 +192,9 @@ class MelissaClimate(ClimateDevice):
|
||||||
"""Translate Melissa states to hass states."""
|
"""Translate Melissa states to hass states."""
|
||||||
if state == self._api.STATE_ON:
|
if state == self._api.STATE_ON:
|
||||||
return STATE_ON
|
return STATE_ON
|
||||||
elif state == self._api.STATE_OFF:
|
if state == self._api.STATE_OFF:
|
||||||
return STATE_OFF
|
return STATE_OFF
|
||||||
elif state == self._api.STATE_IDLE:
|
if state == self._api.STATE_IDLE:
|
||||||
return STATE_IDLE
|
return STATE_IDLE
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -202,11 +202,11 @@ class MelissaClimate(ClimateDevice):
|
||||||
"""Translate Melissa modes to hass states."""
|
"""Translate Melissa modes to hass states."""
|
||||||
if mode == self._api.MODE_HEAT:
|
if mode == self._api.MODE_HEAT:
|
||||||
return STATE_HEAT
|
return STATE_HEAT
|
||||||
elif mode == self._api.MODE_COOL:
|
if mode == self._api.MODE_COOL:
|
||||||
return STATE_COOL
|
return STATE_COOL
|
||||||
elif mode == self._api.MODE_DRY:
|
if mode == self._api.MODE_DRY:
|
||||||
return STATE_DRY
|
return STATE_DRY
|
||||||
elif mode == self._api.MODE_FAN:
|
if mode == self._api.MODE_FAN:
|
||||||
return STATE_FAN_ONLY
|
return STATE_FAN_ONLY
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"Operation mode %s could not be mapped to hass", mode)
|
"Operation mode %s could not be mapped to hass", mode)
|
||||||
|
@ -216,11 +216,11 @@ class MelissaClimate(ClimateDevice):
|
||||||
"""Translate Melissa fan modes to hass modes."""
|
"""Translate Melissa fan modes to hass modes."""
|
||||||
if fan == self._api.FAN_AUTO:
|
if fan == self._api.FAN_AUTO:
|
||||||
return STATE_AUTO
|
return STATE_AUTO
|
||||||
elif fan == self._api.FAN_LOW:
|
if fan == self._api.FAN_LOW:
|
||||||
return SPEED_LOW
|
return SPEED_LOW
|
||||||
elif fan == self._api.FAN_MEDIUM:
|
if fan == self._api.FAN_MEDIUM:
|
||||||
return SPEED_MEDIUM
|
return SPEED_MEDIUM
|
||||||
elif fan == self._api.FAN_HIGH:
|
if fan == self._api.FAN_HIGH:
|
||||||
return SPEED_HIGH
|
return SPEED_HIGH
|
||||||
_LOGGER.warning("Fan mode %s could not be mapped to hass", fan)
|
_LOGGER.warning("Fan mode %s could not be mapped to hass", fan)
|
||||||
return None
|
return None
|
||||||
|
@ -229,24 +229,22 @@ class MelissaClimate(ClimateDevice):
|
||||||
"""Translate hass states to melissa modes."""
|
"""Translate hass states to melissa modes."""
|
||||||
if mode == STATE_HEAT:
|
if mode == STATE_HEAT:
|
||||||
return self._api.MODE_HEAT
|
return self._api.MODE_HEAT
|
||||||
elif mode == STATE_COOL:
|
if mode == STATE_COOL:
|
||||||
return self._api.MODE_COOL
|
return self._api.MODE_COOL
|
||||||
elif mode == STATE_DRY:
|
if mode == STATE_DRY:
|
||||||
return self._api.MODE_DRY
|
return self._api.MODE_DRY
|
||||||
elif mode == STATE_FAN_ONLY:
|
if mode == STATE_FAN_ONLY:
|
||||||
return self._api.MODE_FAN
|
return self._api.MODE_FAN
|
||||||
else:
|
_LOGGER.warning("Melissa have no setting for %s mode", mode)
|
||||||
_LOGGER.warning("Melissa have no setting for %s mode", mode)
|
|
||||||
|
|
||||||
def hass_fan_to_melissa(self, fan):
|
def hass_fan_to_melissa(self, fan):
|
||||||
"""Translate hass fan modes to melissa modes."""
|
"""Translate hass fan modes to melissa modes."""
|
||||||
if fan == STATE_AUTO:
|
if fan == STATE_AUTO:
|
||||||
return self._api.FAN_AUTO
|
return self._api.FAN_AUTO
|
||||||
elif fan == SPEED_LOW:
|
if fan == SPEED_LOW:
|
||||||
return self._api.FAN_LOW
|
return self._api.FAN_LOW
|
||||||
elif fan == SPEED_MEDIUM:
|
if fan == SPEED_MEDIUM:
|
||||||
return self._api.FAN_MEDIUM
|
return self._api.FAN_MEDIUM
|
||||||
elif fan == SPEED_HIGH:
|
if fan == SPEED_HIGH:
|
||||||
return self._api.FAN_HIGH
|
return self._api.FAN_HIGH
|
||||||
else:
|
_LOGGER.warning("Melissa have no setting for %s fan mode", fan)
|
||||||
_LOGGER.warning("Melissa have no setting for %s fan mode", fan)
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ from homeassistant.const import (
|
||||||
from homeassistant.components.climate import (
|
from homeassistant.components.climate import (
|
||||||
ClimateDevice, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE)
|
ClimateDevice, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE)
|
||||||
|
|
||||||
import homeassistant.components.modbus as modbus
|
from homeassistant.components import modbus
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
|
||||||
DEPENDENCIES = ['modbus']
|
DEPENDENCIES = ['modbus']
|
||||||
|
|
|
@ -10,7 +10,7 @@ import logging
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
import homeassistant.components.mqtt as mqtt
|
from homeassistant.components import mqtt
|
||||||
|
|
||||||
from homeassistant.components.climate import (
|
from homeassistant.components.climate import (
|
||||||
STATE_HEAT, STATE_COOL, STATE_DRY, STATE_FAN_ONLY, ClimateDevice,
|
STATE_HEAT, STATE_COOL, STATE_DRY, STATE_FAN_ONLY, ClimateDevice,
|
||||||
|
|
|
@ -147,7 +147,7 @@ class NestThermostat(ClimateDevice):
|
||||||
"""Return current operation ie. heat, cool, idle."""
|
"""Return current operation ie. heat, cool, idle."""
|
||||||
if self._mode in [STATE_HEAT, STATE_COOL, STATE_OFF, STATE_ECO]:
|
if self._mode in [STATE_HEAT, STATE_COOL, STATE_OFF, STATE_ECO]:
|
||||||
return self._mode
|
return self._mode
|
||||||
elif self._mode == NEST_MODE_HEAT_COOL:
|
if self._mode == NEST_MODE_HEAT_COOL:
|
||||||
return STATE_AUTO
|
return STATE_AUTO
|
||||||
return STATE_UNKNOWN
|
return STATE_UNKNOWN
|
||||||
|
|
||||||
|
|
|
@ -99,7 +99,7 @@ class NetatmoThermostat(ClimateDevice):
|
||||||
state = self._data.thermostatdata.relay_cmd
|
state = self._data.thermostatdata.relay_cmd
|
||||||
if state == 0:
|
if state == 0:
|
||||||
return STATE_IDLE
|
return STATE_IDLE
|
||||||
elif state == 100:
|
if state == 100:
|
||||||
return STATE_HEAT
|
return STATE_HEAT
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -140,7 +140,7 @@ class NetatmoThermostat(ClimateDevice):
|
||||||
self._away = self._data.setpoint_mode == 'away'
|
self._away = self._data.setpoint_mode == 'away'
|
||||||
|
|
||||||
|
|
||||||
class ThermostatData(object):
|
class ThermostatData:
|
||||||
"""Get the latest data from Netatmo."""
|
"""Get the latest data from Netatmo."""
|
||||||
|
|
||||||
def __init__(self, auth, device=None):
|
def __init__(self, auth, device=None):
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue