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