Merge branch 'dev' into rc

This commit is contained in:
Paulus Schoutsen 2018-07-30 13:45:35 +02:00
commit be61e2e714
634 changed files with 5774 additions and 2336 deletions

View file

@ -251,6 +251,9 @@ omit =
homeassistant/components/scsgate.py homeassistant/components/scsgate.py
homeassistant/components/*/scsgate.py homeassistant/components/*/scsgate.py
homeassistant/components/sisyphus.py
homeassistant/components/*/sisyphus.py
homeassistant/components/skybell.py homeassistant/components/skybell.py
homeassistant/components/*/skybell.py homeassistant/components/*/skybell.py
@ -346,6 +349,9 @@ omit =
homeassistant/components/tuya.py homeassistant/components/tuya.py
homeassistant/components/*/tuya.py homeassistant/components/*/tuya.py
homeassistant/components/spider.py
homeassistant/components/*/spider.py
homeassistant/components/alarm_control_panel/alarmdotcom.py homeassistant/components/alarm_control_panel/alarmdotcom.py
homeassistant/components/alarm_control_panel/canary.py homeassistant/components/alarm_control_panel/canary.py
homeassistant/components/alarm_control_panel/concord232.py homeassistant/components/alarm_control_panel/concord232.py
@ -398,6 +404,8 @@ omit =
homeassistant/components/climate/touchline.py homeassistant/components/climate/touchline.py
homeassistant/components/climate/venstar.py homeassistant/components/climate/venstar.py
homeassistant/components/climate/zhong_hong.py homeassistant/components/climate/zhong_hong.py
homeassistant/components/cover/aladdin_connect.py
homeassistant/components/cover/brunt.py
homeassistant/components/cover/garadget.py homeassistant/components/cover/garadget.py
homeassistant/components/cover/gogogate2.py homeassistant/components/cover/gogogate2.py
homeassistant/components/cover/homematic.py homeassistant/components/cover/homematic.py
@ -461,6 +469,7 @@ omit =
homeassistant/components/light/decora_wifi.py homeassistant/components/light/decora_wifi.py
homeassistant/components/light/decora.py homeassistant/components/light/decora.py
homeassistant/components/light/flux_led.py homeassistant/components/light/flux_led.py
homeassistant/components/light/futurenow.py
homeassistant/components/light/greenwave.py homeassistant/components/light/greenwave.py
homeassistant/components/light/hue.py homeassistant/components/light/hue.py
homeassistant/components/light/hyperion.py homeassistant/components/light/hyperion.py
@ -657,6 +666,7 @@ omit =
homeassistant/components/sensor/loopenergy.py homeassistant/components/sensor/loopenergy.py
homeassistant/components/sensor/luftdaten.py homeassistant/components/sensor/luftdaten.py
homeassistant/components/sensor/lyft.py homeassistant/components/sensor/lyft.py
homeassistant/components/sensor/magicseaweed.py
homeassistant/components/sensor/metoffice.py homeassistant/components/sensor/metoffice.py
homeassistant/components/sensor/miflora.py homeassistant/components/sensor/miflora.py
homeassistant/components/sensor/mitemp_bt.py homeassistant/components/sensor/mitemp_bt.py

View file

@ -1,6 +1,6 @@
# Contributing to Home Assistant # Contributing to Home Assistant
Everybody is invited and welcome to contribute to Home Assistant. There is a lot to do...if you are not a developer perhaps you would like to help with the documentation on [home-assistant.io](https://home-assistant.io/)? If you are a developer and have devices in your home which aren't working with Home Assistant yet, why not spent a couple of hours and help to integrate them? Everybody is invited and welcome to contribute to Home Assistant. There is a lot to do...if you are not a developer perhaps you would like to help with the documentation on [home-assistant.io](https://home-assistant.io/)? If you are a developer and have devices in your home which aren't working with Home Assistant yet, why not spend a couple of hours and help to integrate them?
The process is straight-forward. The process is straight-forward.

View file

@ -8,7 +8,7 @@ import subprocess
import sys import sys
import threading import threading
from typing import Optional, List, Dict, Any # noqa #pylint: disable=unused-import from typing import List, Dict, Any # noqa pylint: disable=unused-import
from homeassistant import monkey_patch from homeassistant import monkey_patch
@ -20,7 +20,7 @@ from homeassistant.const import (
) )
def attempt_use_uvloop(): def attempt_use_uvloop() -> None:
"""Attempt to use uvloop.""" """Attempt to use uvloop."""
import asyncio import asyncio
@ -280,11 +280,11 @@ def setup_and_run_hass(config_dir: str,
# Imported here to avoid importing asyncio before monkey patch # Imported here to avoid importing asyncio before monkey patch
from homeassistant.util.async_ import run_callback_threadsafe from homeassistant.util.async_ import run_callback_threadsafe
def open_browser(event): def open_browser(_: Any) -> None:
"""Open the webinterface in a browser.""" """Open the web interface in a browser."""
if hass.config.api is not None: if hass.config.api is not None: # type: ignore
import webbrowser import webbrowser
webbrowser.open(hass.config.api.base_url) webbrowser.open(hass.config.api.base_url) # type: ignore
run_callback_threadsafe( run_callback_threadsafe(
hass.loop, hass.loop,

View file

@ -3,6 +3,7 @@ import base64
from collections import OrderedDict from collections import OrderedDict
import hashlib import hashlib
import hmac import hmac
from typing import Dict # noqa: F401 pylint: disable=unused-import
import voluptuous as vol import voluptuous as vol
@ -68,12 +69,12 @@ class Data:
"""Return users.""" """Return users."""
return self._data['users'] return self._data['users']
def validate_login(self, username, password): def validate_login(self, username: str, password: str) -> None:
"""Validate a username and password. """Validate a username and password.
Raises InvalidAuth if auth invalid. Raises InvalidAuth if auth invalid.
""" """
password = self.hash_password(password) hashed = self.hash_password(password)
found = None found = None
@ -84,33 +85,33 @@ class Data:
if found is None: if found is None:
# Do one more compare to make timing the same as if user was found. # Do one more compare to make timing the same as if user was found.
hmac.compare_digest(password, password) hmac.compare_digest(hashed, hashed)
raise InvalidAuth raise InvalidAuth
if not hmac.compare_digest(password, if not hmac.compare_digest(hashed,
base64.b64decode(found['password'])): base64.b64decode(found['password'])):
raise InvalidAuth raise InvalidAuth
def hash_password(self, password, for_storage=False): def hash_password(self, password: str, for_storage: bool = False) -> bytes:
"""Encode a password.""" """Encode a password."""
hashed = hashlib.pbkdf2_hmac( hashed = hashlib.pbkdf2_hmac(
'sha512', password.encode(), self._data['salt'].encode(), 100000) 'sha512', password.encode(), self._data['salt'].encode(), 100000)
if for_storage: if for_storage:
hashed = base64.b64encode(hashed).decode() hashed = base64.b64encode(hashed)
return hashed return hashed
def add_auth(self, username, password): def add_auth(self, username: str, password: str) -> None:
"""Add a new authenticated user/pass.""" """Add a new authenticated user/pass."""
if any(user['username'] == username for user in self.users): if any(user['username'] == username for user in self.users):
raise InvalidUser raise InvalidUser
self.users.append({ self.users.append({
'username': username, 'username': username,
'password': self.hash_password(password, True), 'password': self.hash_password(password, True).decode(),
}) })
@callback @callback
def async_remove_auth(self, username): def async_remove_auth(self, username: str) -> None:
"""Remove authentication.""" """Remove authentication."""
index = None index = None
for i, user in enumerate(self.users): for i, user in enumerate(self.users):
@ -123,14 +124,15 @@ class Data:
self.users.pop(index) self.users.pop(index)
def change_password(self, username, new_password): def change_password(self, username: str, new_password: str) -> None:
"""Update the password. """Update the password.
Raises InvalidUser if user cannot be found. Raises InvalidUser if user cannot be found.
""" """
for user in self.users: for user in self.users:
if user['username'] == username: if user['username'] == username:
user['password'] = self.hash_password(new_password, True) user['password'] = self.hash_password(
new_password, True).decode()
break break
else: else:
raise InvalidUser raise InvalidUser
@ -160,7 +162,7 @@ class HassAuthProvider(AuthProvider):
"""Return a flow to login.""" """Return a flow to login."""
return LoginFlow(self) return LoginFlow(self)
async def async_validate_login(self, username, password): async def async_validate_login(self, username: str, password: str):
"""Helper to validate a username and password.""" """Helper to validate a username and password."""
if self.data is None: if self.data is None:
await self.async_initialize() await self.async_initialize()
@ -225,7 +227,7 @@ class LoginFlow(data_entry_flow.FlowHandler):
data=user_input data=user_input
) )
schema = OrderedDict() schema = OrderedDict() # type: Dict[str, type]
schema['username'] = str schema['username'] = str
schema['password'] = str schema['password'] = str

View file

@ -221,8 +221,8 @@ async def async_from_config_file(config_path: str,
@core.callback @core.callback
def async_enable_logging(hass: core.HomeAssistant, def async_enable_logging(hass: core.HomeAssistant,
verbose: bool = False, verbose: bool = False,
log_rotate_days=None, log_rotate_days: Optional[int] = None,
log_file=None, log_file: Optional[str] = None,
log_no_color: bool = False) -> None: log_no_color: bool = False) -> None:
"""Set up the logging. """Set up the logging.
@ -291,9 +291,9 @@ def async_enable_logging(hass: core.HomeAssistant,
async_handler = AsyncHandler(hass.loop, err_handler) async_handler = AsyncHandler(hass.loop, err_handler)
async def async_stop_async_handler(event): async def async_stop_async_handler(_: Any) -> None:
"""Cleanup async handler.""" """Cleanup async handler."""
logging.getLogger('').removeHandler(async_handler) logging.getLogger('').removeHandler(async_handler) # type: ignore
await async_handler.async_close(blocking=True) await async_handler.async_close(blocking=True)
hass.bus.async_listen_once( hass.bus.async_listen_once(

View file

@ -167,7 +167,7 @@ def async_setup(hass, config):
def async_handle_core_service(call): def async_handle_core_service(call):
"""Service handler for handling core services.""" """Service handler for handling core services."""
if call.service == SERVICE_HOMEASSISTANT_STOP: if call.service == SERVICE_HOMEASSISTANT_STOP:
hass.async_add_job(hass.async_stop()) hass.async_create_task(hass.async_stop())
return return
try: try:
@ -183,7 +183,7 @@ def async_setup(hass, config):
return return
if call.service == SERVICE_HOMEASSISTANT_RESTART: if call.service == SERVICE_HOMEASSISTANT_RESTART:
hass.async_add_job(hass.async_stop(RESTART_EXIT_CODE)) hass.async_create_task(hass.async_stop(RESTART_EXIT_CODE))
hass.services.async_register( hass.services.async_register(
ha.DOMAIN, SERVICE_HOMEASSISTANT_STOP, async_handle_core_service) ha.DOMAIN, SERVICE_HOMEASSISTANT_STOP, async_handle_core_service)

View file

@ -85,7 +85,7 @@ ABODE_PLATFORMS = [
] ]
class AbodeSystem(object): class AbodeSystem:
"""Abode System class.""" """Abode System class."""
def __init__(self, username, password, cache, def __init__(self, username, password, cache,

View file

@ -110,7 +110,7 @@ NotificationItem = namedtuple(
) )
class AdsHub(object): class AdsHub:
"""Representation of an ADS connection.""" """Representation of an ADS connection."""
def __init__(self, ads_client): def __init__(self, ads_client):

View file

@ -187,7 +187,7 @@ class AlarmControlPanel(Entity):
This method must be run in the event loop and returns a coroutine. This method must be run in the event loop and returns a coroutine.
""" """
return self.hass.async_add_job(self.alarm_disarm, code) return self.hass.async_add_executor_job(self.alarm_disarm, code)
def alarm_arm_home(self, code=None): def alarm_arm_home(self, code=None):
"""Send arm home command.""" """Send arm home command."""
@ -198,7 +198,7 @@ class AlarmControlPanel(Entity):
This method must be run in the event loop and returns a coroutine. This method must be run in the event loop and returns a coroutine.
""" """
return self.hass.async_add_job(self.alarm_arm_home, code) return self.hass.async_add_executor_job(self.alarm_arm_home, code)
def alarm_arm_away(self, code=None): def alarm_arm_away(self, code=None):
"""Send arm away command.""" """Send arm away command."""
@ -209,7 +209,7 @@ class AlarmControlPanel(Entity):
This method must be run in the event loop and returns a coroutine. This method must be run in the event loop and returns a coroutine.
""" """
return self.hass.async_add_job(self.alarm_arm_away, code) return self.hass.async_add_executor_job(self.alarm_arm_away, code)
def alarm_arm_night(self, code=None): def alarm_arm_night(self, code=None):
"""Send arm night command.""" """Send arm night command."""
@ -220,7 +220,7 @@ class AlarmControlPanel(Entity):
This method must be run in the event loop and returns a coroutine. This method must be run in the event loop and returns a coroutine.
""" """
return self.hass.async_add_job(self.alarm_arm_night, code) return self.hass.async_add_executor_job(self.alarm_arm_night, code)
def alarm_trigger(self, code=None): def alarm_trigger(self, code=None):
"""Send alarm trigger command.""" """Send alarm trigger command."""
@ -231,7 +231,7 @@ class AlarmControlPanel(Entity):
This method must be run in the event loop and returns a coroutine. This method must be run in the event loop and returns a coroutine.
""" """
return self.hass.async_add_job(self.alarm_trigger, code) return self.hass.async_add_executor_job(self.alarm_trigger, code)
def alarm_arm_custom_bypass(self, code=None): def alarm_arm_custom_bypass(self, code=None):
"""Send arm custom bypass command.""" """Send arm custom bypass command."""
@ -242,7 +242,8 @@ class AlarmControlPanel(Entity):
This method must be run in the event loop and returns a coroutine. This method must be run in the event loop and returns a coroutine.
""" """
return self.hass.async_add_job(self.alarm_arm_custom_bypass, code) return self.hass.async_add_executor_job(
self.alarm_arm_custom_bypass, code)
@property @property
def state_attributes(self): def state_attributes(self):

View file

@ -83,7 +83,7 @@ class AlarmDotCom(alarm.AlarmControlPanel):
"""Return one or more digits/characters.""" """Return one or more digits/characters."""
if self._code is None: if self._code is None:
return None return None
elif isinstance(self._code, str) and re.search('^\\d+$', self._code): if isinstance(self._code, str) and re.search('^\\d+$', self._code):
return 'Number' return 'Number'
return 'Any' return 'Any'
@ -92,9 +92,9 @@ class AlarmDotCom(alarm.AlarmControlPanel):
"""Return the state of the device.""" """Return the state of the device."""
if self._alarm.state.lower() == 'disarmed': if self._alarm.state.lower() == 'disarmed':
return STATE_ALARM_DISARMED return STATE_ALARM_DISARMED
elif self._alarm.state.lower() == 'armed stay': if self._alarm.state.lower() == 'armed stay':
return STATE_ALARM_ARMED_HOME return STATE_ALARM_ARMED_HOME
elif self._alarm.state.lower() == 'armed away': if self._alarm.state.lower() == 'armed away':
return STATE_ALARM_ARMED_AWAY return STATE_ALARM_ARMED_AWAY
return STATE_UNKNOWN return STATE_UNKNOWN

View file

@ -122,10 +122,10 @@ class ArloBaseStation(AlarmControlPanel):
"""Convert Arlo mode to Home Assistant state.""" """Convert Arlo mode to Home Assistant state."""
if mode == ARMED: if mode == ARMED:
return STATE_ALARM_ARMED_AWAY return STATE_ALARM_ARMED_AWAY
elif mode == DISARMED: if mode == DISARMED:
return STATE_ALARM_DISARMED return STATE_ALARM_DISARMED
elif mode == self._home_mode_name: if mode == self._home_mode_name:
return STATE_ALARM_ARMED_HOME return STATE_ALARM_ARMED_HOME
elif mode == self._away_mode_name: if mode == self._away_mode_name:
return STATE_ALARM_ARMED_AWAY return STATE_ALARM_ARMED_AWAY
return mode return mode

View file

@ -55,9 +55,9 @@ class CanaryAlarm(AlarmControlPanel):
mode = location.mode mode = location.mode
if mode.name == LOCATION_MODE_AWAY: if mode.name == LOCATION_MODE_AWAY:
return STATE_ALARM_ARMED_AWAY return STATE_ALARM_ARMED_AWAY
elif mode.name == LOCATION_MODE_HOME: if mode.name == LOCATION_MODE_HOME:
return STATE_ALARM_ARMED_HOME return STATE_ALARM_ARMED_HOME
elif mode.name == LOCATION_MODE_NIGHT: if mode.name == LOCATION_MODE_NIGHT:
return STATE_ALARM_ARMED_NIGHT return STATE_ALARM_ARMED_NIGHT
return None return None

View file

@ -5,7 +5,7 @@ For more details about this platform, please refer to the documentation
https://home-assistant.io/components/demo/ https://home-assistant.io/components/demo/
""" """
import datetime import datetime
import homeassistant.components.alarm_control_panel.manual as manual from homeassistant.components.alarm_control_panel import manual
from homeassistant.const import ( from homeassistant.const import (
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_CUSTOM_BYPASS,
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT,

View file

@ -20,7 +20,6 @@ DEPENDENCIES = ['homematicip_cloud']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
HMIP_OPEN = 'OPEN'
HMIP_ZONE_AWAY = 'EXTERNAL' HMIP_ZONE_AWAY = 'EXTERNAL'
HMIP_ZONE_HOME = 'INTERNAL' HMIP_ZONE_HOME = 'INTERNAL'
@ -57,14 +56,18 @@ class HomematicipSecurityZone(HomematicipGenericDevice, AlarmControlPanel):
@property @property
def state(self): def state(self):
"""Return the state of the device.""" """Return the state of the device."""
from homematicip.base.enums import WindowState
if self._device.active: if self._device.active:
if (self._device.sabotage or self._device.motionDetected or if (self._device.sabotage or self._device.motionDetected or
self._device.windowState == HMIP_OPEN): self._device.windowState == WindowState.OPEN):
return STATE_ALARM_TRIGGERED return STATE_ALARM_TRIGGERED
if self._device.label == HMIP_ZONE_HOME: active = self._home.get_security_zones_activation()
if active == (True, True):
return STATE_ALARM_ARMED_AWAY
if active == (False, True):
return STATE_ALARM_ARMED_HOME return STATE_ALARM_ARMED_HOME
return STATE_ALARM_ARMED_AWAY
return STATE_ALARM_DISARMED return STATE_ALARM_DISARMED
@ -79,10 +82,3 @@ class HomematicipSecurityZone(HomematicipGenericDevice, AlarmControlPanel):
async def async_alarm_arm_away(self, code=None): async def async_alarm_arm_away(self, code=None):
"""Send arm away command.""" """Send arm away command."""
await self._home.set_security_zones_activation(True, True) await self._home.set_security_zones_activation(True, True)
@property
def device_state_attributes(self):
"""Return the state attributes of the alarm control device."""
# The base class is loading the battery property, but device doesn't
# have this property - base class needs clean-up.
return None

View file

@ -128,7 +128,7 @@ class IFTTTAlarmPanel(alarm.AlarmControlPanel):
"""Return one or more digits/characters.""" """Return one or more digits/characters."""
if self._code is None: if self._code is None:
return None return None
elif isinstance(self._code, str) and re.search('^\\d+$', self._code): if isinstance(self._code, str) and re.search('^\\d+$', self._code):
return 'Number' return 'Number'
return 'Any' return 'Any'

View file

@ -205,7 +205,7 @@ class ManualAlarm(alarm.AlarmControlPanel):
"""Return one or more digits/characters.""" """Return one or more digits/characters."""
if self._code is None: if self._code is None:
return None return None
elif isinstance(self._code, str) and re.search('^\\d+$', self._code): if isinstance(self._code, str) and re.search('^\\d+$', self._code):
return 'Number' return 'Number'
return 'Any' return 'Any'

View file

@ -19,7 +19,7 @@ from homeassistant.const import (
STATE_ALARM_DISARMED, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, STATE_ALARM_DISARMED, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED,
CONF_PLATFORM, CONF_NAME, CONF_CODE, CONF_DELAY_TIME, CONF_PENDING_TIME, CONF_PLATFORM, CONF_NAME, CONF_CODE, CONF_DELAY_TIME, CONF_PENDING_TIME,
CONF_TRIGGER_TIME, CONF_DISARM_AFTER_TRIGGER) CONF_TRIGGER_TIME, CONF_DISARM_AFTER_TRIGGER)
import homeassistant.components.mqtt as mqtt from homeassistant.components import mqtt
from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.event import async_track_state_change
from homeassistant.core import callback from homeassistant.core import callback
@ -241,7 +241,7 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
"""Return one or more digits/characters.""" """Return one or more digits/characters."""
if self._code is None: if self._code is None:
return None return None
elif isinstance(self._code, str) and re.search('^\\d+$', self._code): if isinstance(self._code, str) and re.search('^\\d+$', self._code):
return 'Number' return 'Number'
return 'Any' return 'Any'

View file

@ -12,7 +12,7 @@ import voluptuous as vol
from homeassistant.core import callback from homeassistant.core import callback
import homeassistant.components.alarm_control_panel as alarm import homeassistant.components.alarm_control_panel as alarm
import homeassistant.components.mqtt as mqtt from homeassistant.components import mqtt
from homeassistant.const import ( from homeassistant.const import (
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED,
STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, STATE_UNKNOWN, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, STATE_UNKNOWN,
@ -49,6 +49,9 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({
@asyncio.coroutine @asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None): def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the MQTT Alarm Control Panel platform.""" """Set up the MQTT Alarm Control Panel platform."""
if discovery_info is not None:
config = PLATFORM_SCHEMA(discovery_info)
async_add_devices([MqttAlarm( async_add_devices([MqttAlarm(
config.get(CONF_NAME), config.get(CONF_NAME),
config.get(CONF_STATE_TOPIC), config.get(CONF_STATE_TOPIC),
@ -123,7 +126,7 @@ class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel):
"""Return one or more digits/characters.""" """Return one or more digits/characters."""
if self._code is None: if self._code is None:
return None return None
elif isinstance(self._code, str) and re.search('^\\d+$', self._code): if isinstance(self._code, str) and re.search('^\\d+$', self._code):
return 'Number' return 'Number'
return 'Any' return 'Any'

View file

@ -9,23 +9,22 @@ import re
import voluptuous as vol import voluptuous as vol
import homeassistant.components.alarm_control_panel as alarm from homeassistant.components.alarm_control_panel import (
from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA PLATFORM_SCHEMA, AlarmControlPanel)
from homeassistant.const import ( from homeassistant.const import (
CONF_CODE, CONF_NAME, CONF_PASSWORD, CONF_USERNAME, CONF_CODE, CONF_NAME, CONF_PASSWORD, CONF_USERNAME,
EVENT_HOMEASSISTANT_STOP, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
STATE_ALARM_DISARMED, STATE_UNKNOWN) STATE_ALARM_DISARMED, STATE_UNKNOWN)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['simplisafe-python==1.0.5'] REQUIREMENTS = ['simplisafe-python==2.0.2']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'SimpliSafe' DEFAULT_NAME = 'SimpliSafe'
DOMAIN = 'simplisafe'
NOTIFICATION_ID = 'simplisafe_notification' ATTR_ALARM_ACTIVE = "alarm_active"
NOTIFICATION_TITLE = 'SimpliSafe Setup' ATTR_TEMPERATURE = "temperature"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_PASSWORD): cv.string,
@ -37,36 +36,27 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the SimpliSafe platform.""" """Set up the SimpliSafe platform."""
from simplipy.api import SimpliSafeApiInterface, get_systems from simplipy.api import SimpliSafeApiInterface, SimpliSafeAPIException
name = config.get(CONF_NAME) name = config.get(CONF_NAME)
code = config.get(CONF_CODE) code = config.get(CONF_CODE)
username = config.get(CONF_USERNAME) username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD) password = config.get(CONF_PASSWORD)
simplisafe = SimpliSafeApiInterface() try:
status = simplisafe.set_credentials(username, password) simplisafe = SimpliSafeApiInterface(username, password)
if status: except SimpliSafeAPIException:
hass.data[DOMAIN] = simplisafe _LOGGER.error("Failed to setup SimpliSafe")
locations = get_systems(simplisafe) return
for location in locations:
add_devices([SimpliSafeAlarm(location, name, code)])
else:
message = 'Failed to log into SimpliSafe. Check credentials.'
_LOGGER.error(message)
hass.components.persistent_notification.create(
message,
title=NOTIFICATION_TITLE,
notification_id=NOTIFICATION_ID)
return False
def logout(event): systems = []
"""Logout of the SimpliSafe API."""
hass.data[DOMAIN].logout()
hass.bus.listen(EVENT_HOMEASSISTANT_STOP, logout) for system in simplisafe.get_systems():
systems.append(SimpliSafeAlarm(system, name, code))
add_devices(systems)
class SimpliSafeAlarm(alarm.AlarmControlPanel): class SimpliSafeAlarm(AlarmControlPanel):
"""Representation of a SimpliSafe alarm.""" """Representation of a SimpliSafe alarm."""
def __init__(self, simplisafe, name, code): def __init__(self, simplisafe, name, code):
@ -75,31 +65,37 @@ class SimpliSafeAlarm(alarm.AlarmControlPanel):
self._name = name self._name = name
self._code = str(code) if code else None self._code = str(code) if code else None
@property
def unique_id(self):
"""Return the unique ID."""
return self.simplisafe.location_id
@property @property
def name(self): def name(self):
"""Return the name of the device.""" """Return the name of the device."""
if self._name is not None: if self._name is not None:
return self._name return self._name
return 'Alarm {}'.format(self.simplisafe.location_id()) return 'Alarm {}'.format(self.simplisafe.location_id)
@property @property
def code_format(self): def code_format(self):
"""Return one or more digits/characters.""" """Return one or more digits/characters."""
if self._code is None: if self._code is None:
return None return None
elif isinstance(self._code, str) and re.search('^\\d+$', self._code): if isinstance(self._code, str) and re.search('^\\d+$', self._code):
return 'Number' return 'Number'
return 'Any' return 'Any'
@property @property
def state(self): def state(self):
"""Return the state of the device.""" """Return the state of the device."""
status = self.simplisafe.state() status = self.simplisafe.state
if status == 'off': if status.lower() == 'off':
state = STATE_ALARM_DISARMED state = STATE_ALARM_DISARMED
elif status == 'home': elif status.lower() == 'home' or status.lower() == 'home_count':
state = STATE_ALARM_ARMED_HOME state = STATE_ALARM_ARMED_HOME
elif status == 'away': elif (status.lower() == 'away' or status.lower() == 'exitDelay' or
status.lower() == 'away_count'):
state = STATE_ALARM_ARMED_AWAY state = STATE_ALARM_ARMED_AWAY
else: else:
state = STATE_UNKNOWN state = STATE_UNKNOWN
@ -108,14 +104,13 @@ class SimpliSafeAlarm(alarm.AlarmControlPanel):
@property @property
def device_state_attributes(self): def device_state_attributes(self):
"""Return the state attributes.""" """Return the state attributes."""
return { attributes = {}
'alarm': self.simplisafe.alarm(),
'co': self.simplisafe.carbon_monoxide(), attributes[ATTR_ALARM_ACTIVE] = self.simplisafe.alarm_active
'fire': self.simplisafe.fire(), if self.simplisafe.temperature is not None:
'flood': self.simplisafe.flood(), attributes[ATTR_TEMPERATURE] = self.simplisafe.temperature
'last_event': self.simplisafe.last_event(),
'temperature': self.simplisafe.temperature(), return attributes
}
def update(self): def update(self):
"""Update alarm status.""" """Update alarm status."""

View file

@ -34,6 +34,8 @@ CONF_ZONE_NAME = 'name'
CONF_ZONE_TYPE = 'type' CONF_ZONE_TYPE = 'type'
CONF_ZONE_RFID = 'rfid' CONF_ZONE_RFID = 'rfid'
CONF_ZONES = 'zones' CONF_ZONES = 'zones'
CONF_RELAY_ADDR = 'relayaddr'
CONF_RELAY_CHAN = 'relaychan'
DEFAULT_DEVICE_TYPE = 'socket' DEFAULT_DEVICE_TYPE = 'socket'
DEFAULT_DEVICE_HOST = 'localhost' DEFAULT_DEVICE_HOST = 'localhost'
@ -53,6 +55,7 @@ SIGNAL_PANEL_DISARM = 'alarmdecoder.panel_disarm'
SIGNAL_ZONE_FAULT = 'alarmdecoder.zone_fault' SIGNAL_ZONE_FAULT = 'alarmdecoder.zone_fault'
SIGNAL_ZONE_RESTORE = 'alarmdecoder.zone_restore' SIGNAL_ZONE_RESTORE = 'alarmdecoder.zone_restore'
SIGNAL_RFX_MESSAGE = 'alarmdecoder.rfx_message' SIGNAL_RFX_MESSAGE = 'alarmdecoder.rfx_message'
SIGNAL_REL_MESSAGE = 'alarmdecoder.rel_message'
DEVICE_SOCKET_SCHEMA = vol.Schema({ DEVICE_SOCKET_SCHEMA = vol.Schema({
vol.Required(CONF_DEVICE_TYPE): 'socket', vol.Required(CONF_DEVICE_TYPE): 'socket',
@ -71,7 +74,11 @@ ZONE_SCHEMA = vol.Schema({
vol.Required(CONF_ZONE_NAME): cv.string, vol.Required(CONF_ZONE_NAME): cv.string,
vol.Optional(CONF_ZONE_TYPE, vol.Optional(CONF_ZONE_TYPE,
default=DEFAULT_ZONE_TYPE): vol.Any(DEVICE_CLASSES_SCHEMA), default=DEFAULT_ZONE_TYPE): vol.Any(DEVICE_CLASSES_SCHEMA),
vol.Optional(CONF_ZONE_RFID): cv.string}) vol.Optional(CONF_ZONE_RFID): cv.string,
vol.Inclusive(CONF_RELAY_ADDR, 'relaylocation',
'Relay address and channel must exist together'): cv.byte,
vol.Inclusive(CONF_RELAY_CHAN, 'relaylocation',
'Relay address and channel must exist together'): cv.byte})
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({ DOMAIN: vol.Schema({
@ -153,6 +160,11 @@ def setup(hass, config):
hass.helpers.dispatcher.dispatcher_send( hass.helpers.dispatcher.dispatcher_send(
SIGNAL_ZONE_RESTORE, zone) SIGNAL_ZONE_RESTORE, zone)
def handle_rel_message(sender, message):
"""Handle relay message from AlarmDecoder."""
hass.helpers.dispatcher.dispatcher_send(
SIGNAL_REL_MESSAGE, message)
controller = False controller = False
if device_type == 'socket': if device_type == 'socket':
host = device.get(CONF_DEVICE_HOST) host = device.get(CONF_DEVICE_HOST)
@ -171,6 +183,7 @@ def setup(hass, config):
controller.on_zone_fault += zone_fault_callback controller.on_zone_fault += zone_fault_callback
controller.on_zone_restore += zone_restore_callback controller.on_zone_restore += zone_restore_callback
controller.on_close += handle_closed_connection controller.on_close += handle_closed_connection
controller.on_relay_changed += handle_rel_message
hass.data[DATA_AD] = controller hass.data[DATA_AD] = controller

View file

@ -68,7 +68,7 @@ def turn_on(hass, entity_id):
def async_turn_on(hass, entity_id): def async_turn_on(hass, entity_id):
"""Async reset the alert.""" """Async reset the alert."""
data = {ATTR_ENTITY_ID: entity_id} data = {ATTR_ENTITY_ID: entity_id}
hass.async_add_job( hass.async_create_task(
hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data)) hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data))
@ -81,7 +81,7 @@ def turn_off(hass, entity_id):
def async_turn_off(hass, entity_id): def async_turn_off(hass, entity_id):
"""Async acknowledge the alert.""" """Async acknowledge the alert."""
data = {ATTR_ENTITY_ID: entity_id} data = {ATTR_ENTITY_ID: entity_id}
hass.async_add_job( hass.async_create_task(
hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, data)) hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, data))
@ -94,7 +94,7 @@ def toggle(hass, entity_id):
def async_toggle(hass, entity_id): def async_toggle(hass, entity_id):
"""Async toggle acknowledgement of alert.""" """Async toggle acknowledgement of alert."""
data = {ATTR_ENTITY_ID: entity_id} data = {ATTR_ENTITY_ID: entity_id}
hass.async_add_job( hass.async_create_task(
hass.services.async_call(DOMAIN, SERVICE_TOGGLE, data)) hass.services.async_call(DOMAIN, SERVICE_TOGGLE, data))
@ -217,7 +217,7 @@ class Alert(ToggleEntity):
else: else:
yield from self._schedule_notify() yield from self._schedule_notify()
self.hass.async_add_job(self.async_update_ha_state) self.async_schedule_update_ha_state()
@asyncio.coroutine @asyncio.coroutine
def end_alerting(self): def end_alerting(self):
@ -228,7 +228,7 @@ class Alert(ToggleEntity):
self._firing = False self._firing = False
if self._done_message and self._send_done_message: if self._done_message and self._send_done_message:
yield from self._notify_done_message() yield from self._notify_done_message()
self.hass.async_add_job(self.async_update_ha_state) self.async_schedule_update_ha_state()
@asyncio.coroutine @asyncio.coroutine
def _schedule_notify(self): def _schedule_notify(self):

View file

@ -210,7 +210,7 @@ def resolve_slot_synonyms(key, request):
return resolved_value return resolved_value
class AlexaResponse(object): class AlexaResponse:
"""Help generating the response for Alexa.""" """Help generating the response for Alexa."""
def __init__(self, hass, intent_info): def __init__(self, hass, intent_info):

View file

@ -55,7 +55,7 @@ HANDLERS = Registry()
ENTITY_ADAPTERS = Registry() ENTITY_ADAPTERS = Registry()
class _DisplayCategory(object): class _DisplayCategory:
"""Possible display categories for Discovery response. """Possible display categories for Discovery response.
https://developer.amazon.com/docs/device-apis/alexa-discovery.html#display-categories https://developer.amazon.com/docs/device-apis/alexa-discovery.html#display-categories
@ -153,7 +153,7 @@ class _UnsupportedProperty(Exception):
"""This entity does not support the requested Smart Home API property.""" """This entity does not support the requested Smart Home API property."""
class _AlexaEntity(object): class _AlexaEntity:
"""An adaptation of an entity, expressed in Alexa's terms. """An adaptation of an entity, expressed in Alexa's terms.
The API handlers should manipulate entities only through this interface. The API handlers should manipulate entities only through this interface.
@ -208,7 +208,7 @@ class _AlexaEntity(object):
raise NotImplementedError raise NotImplementedError
class _AlexaInterface(object): class _AlexaInterface:
def __init__(self, entity): def __init__(self, entity):
self.entity = entity self.entity = entity
@ -315,7 +315,7 @@ class _AlexaLockController(_AlexaInterface):
if self.entity.state == STATE_LOCKED: if self.entity.state == STATE_LOCKED:
return 'LOCKED' return 'LOCKED'
elif self.entity.state == STATE_UNLOCKED: if self.entity.state == STATE_UNLOCKED:
return 'UNLOCKED' return 'UNLOCKED'
return 'JAMMED' return 'JAMMED'
@ -615,7 +615,7 @@ class _SensorCapabilities(_AlexaEntity):
yield _AlexaTemperatureSensor(self.entity) yield _AlexaTemperatureSensor(self.entity)
class _Cause(object): class _Cause:
"""Possible causes for property changes. """Possible causes for property changes.
https://developer.amazon.com/docs/smarthome/state-reporting-for-a-smart-home-skill.html#cause-object https://developer.amazon.com/docs/smarthome/state-reporting-for-a-smart-home-skill.html#cause-object

View file

@ -164,7 +164,7 @@ def setup(hass, config):
return True return True
class AmcrestDevice(object): class AmcrestDevice:
"""Representation of a base Amcrest discovery device.""" """Representation of a base Amcrest discovery device."""
def __init__(self, camera, name, authentication, ffmpeg_arguments, def __init__(self, camera, name, authentication, ffmpeg_arguments,

View file

@ -214,11 +214,11 @@ def async_setup(hass, config):
CONF_PASSWORD: password CONF_PASSWORD: password
}) })
hass.async_add_job(discovery.async_load_platform( hass.async_create_task(discovery.async_load_platform(
hass, 'camera', 'mjpeg', mjpeg_camera, config)) hass, 'camera', 'mjpeg', mjpeg_camera, config))
if sensors: if sensors:
hass.async_add_job(discovery.async_load_platform( hass.async_create_task(discovery.async_load_platform(
hass, 'sensor', DOMAIN, { hass, 'sensor', DOMAIN, {
CONF_NAME: name, CONF_NAME: name,
CONF_HOST: host, CONF_HOST: host,
@ -226,7 +226,7 @@ def async_setup(hass, config):
}, config)) }, config))
if switches: if switches:
hass.async_add_job(discovery.async_load_platform( hass.async_create_task(discovery.async_load_platform(
hass, 'switch', DOMAIN, { hass, 'switch', DOMAIN, {
CONF_NAME: name, CONF_NAME: name,
CONF_HOST: host, CONF_HOST: host,
@ -234,7 +234,7 @@ def async_setup(hass, config):
}, config)) }, config))
if motion: if motion:
hass.async_add_job(discovery.async_load_platform( hass.async_create_task(discovery.async_load_platform(
hass, 'binary_sensor', DOMAIN, { hass, 'binary_sensor', DOMAIN, {
CONF_HOST: host, CONF_HOST: host,
CONF_NAME: name, CONF_NAME: name,

View file

@ -58,7 +58,7 @@ def setup(hass, config):
return True return True
class APCUPSdData(object): class APCUPSdData:
"""Stores the data retrieved from APCUPSd. """Stores the data retrieved from APCUPSd.
For each entity to use, acts as the single point responsible for fetching For each entity to use, acts as the single point responsible for fetching

View file

@ -220,7 +220,8 @@ class APIEntityStateView(HomeAssistantView):
is_new_state = hass.states.get(entity_id) is None is_new_state = hass.states.get(entity_id) is None
# Write state # Write state
hass.states.async_set(entity_id, new_state, attributes, force_update) hass.states.async_set(entity_id, new_state, attributes, force_update,
self.context(request))
# Read the state back for our response # Read the state back for our response
status_code = HTTP_CREATED if is_new_state else 200 status_code = HTTP_CREATED if is_new_state else 200
@ -279,7 +280,8 @@ class APIEventView(HomeAssistantView):
event_data[key] = state event_data[key] = state
request.app['hass'].bus.async_fire( request.app['hass'].bus.async_fire(
event_type, event_data, ha.EventOrigin.remote) event_type, event_data, ha.EventOrigin.remote,
self.context(request))
return self.json_message("Event {} fired.".format(event_type)) return self.json_message("Event {} fired.".format(event_type))
@ -316,7 +318,8 @@ class APIDomainServicesView(HomeAssistantView):
"Data should be valid JSON.", HTTP_BAD_REQUEST) "Data should be valid JSON.", HTTP_BAD_REQUEST)
with AsyncTrackStates(hass) as changed_states: with AsyncTrackStates(hass) as changed_states:
await hass.services.async_call(domain, service, data, True) await hass.services.async_call(
domain, service, data, True, self.context(request))
return self.json(changed_states) return self.json(changed_states)

View file

@ -45,7 +45,7 @@ NOTIFICATION_AUTH_TITLE = 'Apple TV Authentication'
NOTIFICATION_SCAN_ID = 'apple_tv_scan_notification' NOTIFICATION_SCAN_ID = 'apple_tv_scan_notification'
NOTIFICATION_SCAN_TITLE = 'Apple TV Scan' NOTIFICATION_SCAN_TITLE = 'Apple TV Scan'
T = TypeVar('T') T = TypeVar('T') # pylint: disable=invalid-name
# This version of ensure_list interprets an empty dict as no value # This version of ensure_list interprets an empty dict as no value
@ -218,10 +218,10 @@ def _setup_atv(hass, atv_config):
ATTR_POWER: power ATTR_POWER: power
} }
hass.async_add_job(discovery.async_load_platform( hass.async_create_task(discovery.async_load_platform(
hass, 'media_player', DOMAIN, atv_config)) hass, 'media_player', DOMAIN, atv_config))
hass.async_add_job(discovery.async_load_platform( hass.async_create_task(discovery.async_load_platform(
hass, 'remote', DOMAIN, atv_config)) hass, 'remote', DOMAIN, atv_config))

View file

@ -62,7 +62,7 @@ def setup(hass, config):
return True return True
class ArduinoBoard(object): class ArduinoBoard:
"""Representation of an Arduino board.""" """Representation of an Arduino board."""
def __init__(self, port): def __init__(self, port):

View file

@ -16,7 +16,7 @@ from homeassistant.const import (
from homeassistant.helpers.event import track_time_interval from homeassistant.helpers.event import track_time_interval
from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.dispatcher import dispatcher_send
REQUIREMENTS = ['pyarlo==0.1.9'] REQUIREMENTS = ['pyarlo==0.2.0']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View file

@ -48,7 +48,7 @@ def setup(hass, config):
return True return True
class AsteriskData(object): class AsteriskData:
"""Store Asterisk mailbox data.""" """Store Asterisk mailbox data."""
def __init__(self, hass, host, port, password): def __init__(self, hass, host, port, password):

View file

@ -123,9 +123,9 @@ def setup_august(hass, config, api, authenticator):
discovery.load_platform(hass, component, DOMAIN, {}, config) discovery.load_platform(hass, component, DOMAIN, {}, config)
return True return True
elif state == AuthenticationState.BAD_PASSWORD: if state == AuthenticationState.BAD_PASSWORD:
return False return False
elif state == AuthenticationState.REQUIRES_VALIDATION: if state == AuthenticationState.REQUIRES_VALIDATION:
request_configuration(hass, config, api, authenticator) request_configuration(hass, config, api, authenticator)
return True return True

View file

@ -1,62 +1,5 @@
"""Component to allow users to login and get tokens. """Component to allow users to login and get tokens.
All requests will require passing in a valid client ID and secret via HTTP
Basic Auth.
# GET /auth/providers
Return a list of auth providers. Example:
[
{
"name": "Local",
"id": null,
"type": "local_provider",
}
]
# POST /auth/login_flow
Create a login flow. Will return the first step of the flow.
Pass in parameter 'handler' to specify the auth provider to use. Auth providers
are identified by type and id.
{
"handler": ["local_provider", null]
}
Return value will be a step in a data entry flow. See the docs for data entry
flow for details.
{
"data_schema": [
{"name": "username", "type": "string"},
{"name": "password", "type": "string"}
],
"errors": {},
"flow_id": "8f7e42faab604bcab7ac43c44ca34d58",
"handler": ["insecure_example", null],
"step_id": "init",
"type": "form"
}
# POST /auth/login_flow/{flow_id}
Progress the flow. Most flows will be 1 page, but could optionally add extra
login challenges, like TFA. Once the flow has finished, the returned step will
have type "create_entry" and "result" key will contain an authorization code.
{
"flow_id": "8f7e42faab604bcab7ac43c44ca34d58",
"handler": ["insecure_example", null],
"result": "411ee2f916e648d691e937ae9344681e",
"source": "user",
"title": "Example",
"type": "create_entry",
"version": 1
}
# POST /auth/token # POST /auth/token
This is an OAuth2 endpoint for granting tokens. We currently support the grant This is an OAuth2 endpoint for granting tokens. We currently support the grant
@ -102,24 +45,20 @@ a limited expiration.
"token_type": "Bearer" "token_type": "Bearer"
} }
""" """
from datetime import timedelta
import logging import logging
import uuid import uuid
from datetime import timedelta
import aiohttp.web
import voluptuous as vol import voluptuous as vol
from homeassistant import data_entry_flow
from homeassistant.core import callback
from homeassistant.helpers.data_entry_flow import (
FlowManagerIndexView, FlowManagerResourceView)
from homeassistant.components import websocket_api from homeassistant.components import websocket_api
from homeassistant.components.http.view import HomeAssistantView from homeassistant.components.http.ban import log_invalid_auth
from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.components.http.data_validator import RequestDataValidator
from homeassistant.components.http.view import HomeAssistantView
from homeassistant.core import callback
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from . import indieauth from . import indieauth
from . import login_flow
DOMAIN = 'auth' DOMAIN = 'auth'
DEPENDENCIES = ['http'] DEPENDENCIES = ['http']
@ -136,10 +75,6 @@ async def async_setup(hass, config):
"""Component to allow users to login.""" """Component to allow users to login."""
store_credentials, retrieve_credentials = _create_cred_store() store_credentials, retrieve_credentials = _create_cred_store()
hass.http.register_view(AuthProvidersView)
hass.http.register_view(LoginFlowIndexView(hass.auth.login_flow))
hass.http.register_view(
LoginFlowResourceView(hass.auth.login_flow, store_credentials))
hass.http.register_view(GrantTokenView(retrieve_credentials)) hass.http.register_view(GrantTokenView(retrieve_credentials))
hass.http.register_view(LinkUserView(retrieve_credentials)) hass.http.register_view(LinkUserView(retrieve_credentials))
@ -148,93 +83,11 @@ async def async_setup(hass, config):
SCHEMA_WS_CURRENT_USER SCHEMA_WS_CURRENT_USER
) )
await login_flow.async_setup(hass, store_credentials)
return True return True
class AuthProvidersView(HomeAssistantView):
"""View to get available auth providers."""
url = '/auth/providers'
name = 'api:auth:providers'
requires_auth = False
async def get(self, request):
"""Get available auth providers."""
return self.json([{
'name': provider.name,
'id': provider.id,
'type': provider.type,
} for provider in request.app['hass'].auth.auth_providers])
class LoginFlowIndexView(FlowManagerIndexView):
"""View to create a config flow."""
url = '/auth/login_flow'
name = 'api:auth:login_flow'
requires_auth = False
async def get(self, request):
"""Do not allow index of flows in progress."""
return aiohttp.web.Response(status=405)
@RequestDataValidator(vol.Schema({
vol.Required('client_id'): str,
vol.Required('handler'): vol.Any(str, list),
vol.Required('redirect_uri'): str,
}))
async def post(self, request, data):
"""Create a new login flow."""
if not indieauth.verify_redirect_uri(data['client_id'],
data['redirect_uri']):
return self.json_message('invalid client id or redirect uri', 400)
# pylint: disable=no-value-for-parameter
return await super().post(request)
class LoginFlowResourceView(FlowManagerResourceView):
"""View to interact with the flow manager."""
url = '/auth/login_flow/{flow_id}'
name = 'api:auth:login_flow:resource'
requires_auth = False
def __init__(self, flow_mgr, store_credentials):
"""Initialize the login flow resource view."""
super().__init__(flow_mgr)
self._store_credentials = store_credentials
async def get(self, request, flow_id):
"""Do not allow getting status of a flow in progress."""
return self.json_message('Invalid flow specified', 404)
@RequestDataValidator(vol.Schema({
'client_id': str
}, extra=vol.ALLOW_EXTRA))
async def post(self, request, flow_id, data):
"""Handle progressing a login flow request."""
client_id = data.pop('client_id')
if not indieauth.verify_client_id(client_id):
return self.json_message('Invalid client id', 400)
try:
result = await self._flow_mgr.async_configure(flow_id, data)
except data_entry_flow.UnknownFlow:
return self.json_message('Invalid flow specified', 404)
except vol.Invalid:
return self.json_message('User input malformed', 400)
if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
return self.json(self._prepare_result_json(result))
result.pop('data')
result['result'] = self._store_credentials(client_id, result['result'])
return self.json(result)
class GrantTokenView(HomeAssistantView): class GrantTokenView(HomeAssistantView):
"""View to grant tokens.""" """View to grant tokens."""
@ -247,11 +100,26 @@ class GrantTokenView(HomeAssistantView):
"""Initialize the grant token view.""" """Initialize the grant token view."""
self._retrieve_credentials = retrieve_credentials self._retrieve_credentials = retrieve_credentials
@log_invalid_auth
async def post(self, request): async def post(self, request):
"""Grant a token.""" """Grant a token."""
hass = request.app['hass'] hass = request.app['hass']
data = await request.post() data = await request.post()
grant_type = data.get('grant_type')
if grant_type == 'authorization_code':
return await self._async_handle_auth_code(hass, data)
if grant_type == 'refresh_token':
return await self._async_handle_refresh_token(hass, data)
return self.json({
'error': 'unsupported_grant_type',
}, status_code=400)
async def _async_handle_auth_code(self, hass, data):
"""Handle authorization code request."""
client_id = data.get('client_id') client_id = data.get('client_id')
if client_id is None or not indieauth.verify_client_id(client_id): if client_id is None or not indieauth.verify_client_id(client_id):
return self.json({ return self.json({
@ -259,21 +127,6 @@ class GrantTokenView(HomeAssistantView):
'error_description': 'Invalid client id', 'error_description': 'Invalid client id',
}, status_code=400) }, status_code=400)
grant_type = data.get('grant_type')
if grant_type == 'authorization_code':
return await self._async_handle_auth_code(hass, client_id, data)
elif grant_type == 'refresh_token':
return await self._async_handle_refresh_token(
hass, client_id, data)
return self.json({
'error': 'unsupported_grant_type',
}, status_code=400)
async def _async_handle_auth_code(self, hass, client_id, data):
"""Handle authorization code request."""
code = data.get('code') code = data.get('code')
if code is None: if code is None:
@ -309,8 +162,15 @@ class GrantTokenView(HomeAssistantView):
int(refresh_token.access_token_expiration.total_seconds()), int(refresh_token.access_token_expiration.total_seconds()),
}) })
async def _async_handle_refresh_token(self, hass, client_id, data): async def _async_handle_refresh_token(self, hass, data):
"""Handle authorization code request.""" """Handle authorization code request."""
client_id = data.get('client_id')
if client_id is not None and not indieauth.verify_client_id(client_id):
return self.json({
'error': 'invalid_request',
'error_description': 'Invalid client id',
}, status_code=400)
token = data.get('refresh_token') token = data.get('refresh_token')
if token is None: if token is None:
@ -320,11 +180,16 @@ class GrantTokenView(HomeAssistantView):
refresh_token = await hass.auth.async_get_refresh_token(token) refresh_token = await hass.auth.async_get_refresh_token(token)
if refresh_token is None or refresh_token.client_id != client_id: if refresh_token is None:
return self.json({ return self.json({
'error': 'invalid_grant', 'error': 'invalid_grant',
}, status_code=400) }, status_code=400)
if refresh_token.client_id != client_id:
return self.json({
'error': 'invalid_request',
}, status_code=400)
access_token = hass.auth.async_create_access_token(refresh_token) access_token = hass.auth.async_create_access_token(refresh_token)
return self.json({ return self.json({
@ -412,4 +277,7 @@ def websocket_current_user(hass, connection, msg):
'id': user.id, 'id': user.id,
'name': user.name, 'name': user.name,
'is_owner': user.is_owner, 'is_owner': user.is_owner,
'credentials': [{'auth_provider_type': c.auth_provider_type,
'auth_provider_id': c.auth_provider_id}
for c in user.credentials]
})) }))

View 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)

View file

@ -297,7 +297,7 @@ class AutomationEntity(ToggleEntity):
return return
# HomeAssistant is starting up # HomeAssistant is starting up
elif self.hass.state == CoreState.not_running: if self.hass.state == CoreState.not_running:
@asyncio.coroutine @asyncio.coroutine
def async_enable_automation(event): def async_enable_automation(event):
"""Start automation on startup.""" """Start automation on startup."""

View file

@ -44,7 +44,7 @@ def async_trigger(hass, config, action):
# Automation are enabled while hass is starting up, fire right away # Automation are enabled while hass is starting up, fire right away
# Check state because a config reload shouldn't trigger it. # Check state because a config reload shouldn't trigger it.
elif hass.state == CoreState.starting: if hass.state == CoreState.starting:
hass.async_run_job(action, { hass.async_run_job(action, {
'trigger': { 'trigger': {
'platform': 'homeassistant', 'platform': 'homeassistant',

View file

@ -10,7 +10,7 @@ import json
import voluptuous as vol import voluptuous as vol
from homeassistant.core import callback from homeassistant.core import callback
import homeassistant.components.mqtt as mqtt from homeassistant.components import mqtt
from homeassistant.const import (CONF_PLATFORM, CONF_PAYLOAD) from homeassistant.const import (CONF_PLATFORM, CONF_PAYLOAD)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv

View file

@ -19,7 +19,7 @@ DOMAIN = 'bbb_gpio'
def setup(hass, config): def setup(hass, config):
"""Set up the BeagleBone Black GPIO component.""" """Set up the BeagleBone Black GPIO component."""
# pylint: disable=import-error # pylint: disable=import-error
import Adafruit_BBIO.GPIO as GPIO from Adafruit_BBIO import GPIO
def cleanup_gpio(event): def cleanup_gpio(event):
"""Stuff to do before stopping.""" """Stuff to do before stopping."""
@ -36,14 +36,14 @@ def setup(hass, config):
def setup_output(pin): def setup_output(pin):
"""Set up a GPIO as output.""" """Set up a GPIO as output."""
# pylint: disable=import-error # pylint: disable=import-error
import Adafruit_BBIO.GPIO as GPIO from Adafruit_BBIO import GPIO
GPIO.setup(pin, GPIO.OUT) GPIO.setup(pin, GPIO.OUT)
def setup_input(pin, pull_mode): def setup_input(pin, pull_mode):
"""Set up a GPIO as input.""" """Set up a GPIO as input."""
# pylint: disable=import-error # pylint: disable=import-error
import Adafruit_BBIO.GPIO as GPIO from Adafruit_BBIO import GPIO
GPIO.setup(pin, GPIO.IN, GPIO.setup(pin, GPIO.IN,
GPIO.PUD_DOWN if pull_mode == 'DOWN' GPIO.PUD_DOWN if pull_mode == 'DOWN'
else GPIO.PUD_UP) else GPIO.PUD_UP)
@ -52,20 +52,20 @@ def setup_input(pin, pull_mode):
def write_output(pin, value): def write_output(pin, value):
"""Write a value to a GPIO.""" """Write a value to a GPIO."""
# pylint: disable=import-error # pylint: disable=import-error
import Adafruit_BBIO.GPIO as GPIO from Adafruit_BBIO import GPIO
GPIO.output(pin, value) GPIO.output(pin, value)
def read_input(pin): def read_input(pin):
"""Read a value from a GPIO.""" """Read a value from a GPIO."""
# pylint: disable=import-error # pylint: disable=import-error
import Adafruit_BBIO.GPIO as GPIO from Adafruit_BBIO import GPIO
return GPIO.input(pin) is GPIO.HIGH return GPIO.input(pin) is GPIO.HIGH
def edge_detect(pin, event_callback, bounce): def edge_detect(pin, event_callback, bounce):
"""Add detection for RISING and FALLING events.""" """Add detection for RISING and FALLING events."""
# pylint: disable=import-error # pylint: disable=import-error
import Adafruit_BBIO.GPIO as GPIO from Adafruit_BBIO import GPIO
GPIO.add_event_detect( GPIO.add_event_detect(
pin, GPIO.BOTH, callback=event_callback, bouncetime=bounce) pin, GPIO.BOTH, callback=event_callback, bouncetime=bounce)

View file

@ -11,7 +11,8 @@ from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.alarmdecoder import ( from homeassistant.components.alarmdecoder import (
ZONE_SCHEMA, CONF_ZONES, CONF_ZONE_NAME, CONF_ZONE_TYPE, ZONE_SCHEMA, CONF_ZONES, CONF_ZONE_NAME, CONF_ZONE_TYPE,
CONF_ZONE_RFID, SIGNAL_ZONE_FAULT, SIGNAL_ZONE_RESTORE, CONF_ZONE_RFID, SIGNAL_ZONE_FAULT, SIGNAL_ZONE_RESTORE,
SIGNAL_RFX_MESSAGE) SIGNAL_RFX_MESSAGE, SIGNAL_REL_MESSAGE, CONF_RELAY_ADDR,
CONF_RELAY_CHAN)
DEPENDENCIES = ['alarmdecoder'] DEPENDENCIES = ['alarmdecoder']
@ -37,8 +38,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
zone_type = device_config_data[CONF_ZONE_TYPE] zone_type = device_config_data[CONF_ZONE_TYPE]
zone_name = device_config_data[CONF_ZONE_NAME] zone_name = device_config_data[CONF_ZONE_NAME]
zone_rfid = device_config_data.get(CONF_ZONE_RFID) zone_rfid = device_config_data.get(CONF_ZONE_RFID)
relay_addr = device_config_data.get(CONF_RELAY_ADDR)
relay_chan = device_config_data.get(CONF_RELAY_CHAN)
device = AlarmDecoderBinarySensor( device = AlarmDecoderBinarySensor(
zone_num, zone_name, zone_type, zone_rfid) zone_num, zone_name, zone_type, zone_rfid, relay_addr, relay_chan)
devices.append(device) devices.append(device)
add_devices(devices) add_devices(devices)
@ -49,7 +52,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class AlarmDecoderBinarySensor(BinarySensorDevice): class AlarmDecoderBinarySensor(BinarySensorDevice):
"""Representation of an AlarmDecoder binary sensor.""" """Representation of an AlarmDecoder binary sensor."""
def __init__(self, zone_number, zone_name, zone_type, zone_rfid): def __init__(self, zone_number, zone_name, zone_type, zone_rfid,
relay_addr, relay_chan):
"""Initialize the binary_sensor.""" """Initialize the binary_sensor."""
self._zone_number = zone_number self._zone_number = zone_number
self._zone_type = zone_type self._zone_type = zone_type
@ -57,6 +61,8 @@ class AlarmDecoderBinarySensor(BinarySensorDevice):
self._name = zone_name self._name = zone_name
self._rfid = zone_rfid self._rfid = zone_rfid
self._rfstate = None self._rfstate = None
self._relay_addr = relay_addr
self._relay_chan = relay_chan
@asyncio.coroutine @asyncio.coroutine
def async_added_to_hass(self): def async_added_to_hass(self):
@ -70,6 +76,9 @@ class AlarmDecoderBinarySensor(BinarySensorDevice):
self.hass.helpers.dispatcher.async_dispatcher_connect( self.hass.helpers.dispatcher.async_dispatcher_connect(
SIGNAL_RFX_MESSAGE, self._rfx_message_callback) SIGNAL_RFX_MESSAGE, self._rfx_message_callback)
self.hass.helpers.dispatcher.async_dispatcher_connect(
SIGNAL_REL_MESSAGE, self._rel_message_callback)
@property @property
def name(self): def name(self):
"""Return the name of the entity.""" """Return the name of the entity."""
@ -122,3 +131,12 @@ class AlarmDecoderBinarySensor(BinarySensorDevice):
if self._rfid and message and message.serial_number == self._rfid: if self._rfid and message and message.serial_number == self._rfid:
self._rfstate = message.value self._rfstate = message.value
self.schedule_update_ha_state() self.schedule_update_ha_state()
def _rel_message_callback(self, message):
"""Update relay state."""
if (self._relay_addr == message.address and
self._relay_chan == message.channel):
_LOGGER.debug("Relay %d:%d value:%d", message.address,
message.channel, message.value)
self._state = message.value
self.schedule_update_ha_state()

View file

@ -89,7 +89,7 @@ class ArestBinarySensor(BinarySensorDevice):
self.arest.update() self.arest.update()
class ArestData(object): class ArestData:
"""Class for handling the data retrieval for pins.""" """Class for handling the data retrieval for pins."""
def __init__(self, resource, pin): def __init__(self, resource, pin):

View file

@ -99,7 +99,7 @@ class AuroraSensor(BinarySensorDevice):
self.aurora_data.update() self.aurora_data.update()
class AuroraData(object): class AuroraData:
"""Get aurora forecast.""" """Get aurora forecast."""
def __init__(self, latitude, longitude, threshold): def __init__(self, latitude, longitude, threshold):

View file

@ -8,7 +8,7 @@ import logging
import voluptuous as vol import voluptuous as vol
import homeassistant.components.bbb_gpio as bbb_gpio from homeassistant.components import bbb_gpio
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA) BinarySensorDevice, PLATFORM_SCHEMA)
from homeassistant.const import (DEVICE_DEFAULT_NAME, CONF_NAME) from homeassistant.const import (DEVICE_DEFAULT_NAME, CONF_NAME)

View file

@ -25,6 +25,9 @@ DEFAULT_PAYLOAD_OFF = 'OFF'
SCAN_INTERVAL = timedelta(seconds=60) SCAN_INTERVAL = timedelta(seconds=60)
CONF_COMMAND_TIMEOUT = 'command_timeout'
DEFAULT_TIMEOUT = 15
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_COMMAND): cv.string, vol.Required(CONF_COMMAND): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
@ -32,6 +35,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
vol.Optional(
CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
}) })
@ -43,9 +48,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
payload_on = config.get(CONF_PAYLOAD_ON) payload_on = config.get(CONF_PAYLOAD_ON)
device_class = config.get(CONF_DEVICE_CLASS) device_class = config.get(CONF_DEVICE_CLASS)
value_template = config.get(CONF_VALUE_TEMPLATE) value_template = config.get(CONF_VALUE_TEMPLATE)
command_timeout = config.get(CONF_COMMAND_TIMEOUT)
if value_template is not None: if value_template is not None:
value_template.hass = hass value_template.hass = hass
data = CommandSensorData(hass, command) data = CommandSensorData(hass, command, command_timeout)
add_devices([CommandBinarySensor( add_devices([CommandBinarySensor(
hass, data, name, device_class, payload_on, payload_off, hass, data, name, device_class, payload_on, payload_off,

View file

@ -117,7 +117,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
add_entities(entities) add_entities(entities)
class HikvisionData(object): class HikvisionData:
"""Hikvision device event stream object.""" """Hikvision device event stream object."""
def __init__(self, hass, url, port, name, username, password): def __init__(self, hass, url, port, name, username, password):

View file

@ -3,8 +3,6 @@
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.ihc/ https://home-assistant.io/components/binary_sensor.ihc/
""" """
from xml.etree.ElementTree import Element
import voluptuous as vol import voluptuous as vol
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
@ -70,7 +68,7 @@ class IHCBinarySensor(IHCDevice, BinarySensorDevice):
def __init__(self, ihc_controller, name, ihc_id: int, info: bool, def __init__(self, ihc_controller, name, ihc_id: int, info: bool,
sensor_type: str, inverting: bool, sensor_type: str, inverting: bool,
product: Element = None) -> None: product=None) -> None:
"""Initialize the IHC binary sensor.""" """Initialize the IHC binary sensor."""
super().__init__(ihc_controller, name, ihc_id, info, product) super().__init__(ihc_controller, name, ihc_id, info, product)
self._state = None self._state = None

View file

@ -17,7 +17,9 @@ _LOGGER = logging.getLogger(__name__)
SENSOR_TYPES = {'openClosedSensor': 'opening', SENSOR_TYPES = {'openClosedSensor': 'opening',
'motionSensor': 'motion', 'motionSensor': 'motion',
'doorSensor': 'door', 'doorSensor': 'door',
'wetLeakSensor': 'moisture'} 'wetLeakSensor': 'moisture',
'lightSensor': 'light',
'batterySensor': 'battery'}
@asyncio.coroutine @asyncio.coroutine
@ -54,4 +56,9 @@ class InsteonPLMBinarySensor(InsteonPLMEntity, BinarySensorDevice):
@property @property
def is_on(self): def is_on(self):
"""Return the boolean response if the node is on.""" """Return the boolean response if the node is on."""
return bool(self._insteon_device_state.value) on_val = bool(self._insteon_device_state.value)
if self._insteon_device_state.name == 'lightSensor':
return not on_val
return on_val

View file

@ -101,7 +101,7 @@ class IssBinarySensor(BinarySensorDevice):
self.iss_data.update() self.iss_data.update()
class IssData(object): class IssData:
"""Get data from the ISS API.""" """Get data from the ISS API."""
def __init__(self, latitude, longitude): def __init__(self, latitude, longitude):

View file

@ -55,7 +55,7 @@ def setup_platform(hass, config: ConfigType,
else: else:
device_type = _detect_device_type(node) device_type = _detect_device_type(node)
subnode_id = int(node.nid[-1]) subnode_id = int(node.nid[-1])
if (device_type == 'opening' or device_type == 'moisture'): if device_type in ('opening', 'moisture'):
# These sensors use an optional "negative" subnode 2 to snag # These sensors use an optional "negative" subnode 2 to snag
# all state changes # all state changes
if subnode_id == 2: if subnode_id == 2:

View file

@ -7,7 +7,7 @@ https://home-assistant.io/components/binary_sensor.modbus/
import logging import logging
import voluptuous as vol import voluptuous as vol
import homeassistant.components.modbus as modbus from homeassistant.components import modbus
from homeassistant.const import CONF_NAME, CONF_SLAVE from homeassistant.const import CONF_NAME, CONF_SLAVE
from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv

View file

@ -11,7 +11,7 @@ from typing import Optional
import voluptuous as vol import voluptuous as vol
from homeassistant.core import callback from homeassistant.core import callback
import homeassistant.components.mqtt as mqtt from homeassistant.components import mqtt
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
BinarySensorDevice, DEVICE_CLASSES_SCHEMA) BinarySensorDevice, DEVICE_CLASSES_SCHEMA)
from homeassistant.const import ( from homeassistant.const import (

View file

@ -142,7 +142,7 @@ class NetatmoBinarySensor(BinarySensorDevice):
"""Return the class of this sensor, from DEVICE_CLASSES.""" """Return the class of this sensor, from DEVICE_CLASSES."""
if self._cameratype == 'NACamera': if self._cameratype == 'NACamera':
return WELCOME_SENSOR_TYPES.get(self._sensor_name) return WELCOME_SENSOR_TYPES.get(self._sensor_name)
elif self._cameratype == 'NOC': if self._cameratype == 'NOC':
return PRESENCE_SENSOR_TYPES.get(self._sensor_name) return PRESENCE_SENSOR_TYPES.get(self._sensor_name)
return TAG_SENSOR_TYPES.get(self._sensor_name) return TAG_SENSOR_TYPES.get(self._sensor_name)

View file

@ -96,7 +96,7 @@ class PingBinarySensor(BinarySensorDevice):
self.ping.update() self.ping.update()
class PingData(object): class PingData:
"""The Class for handling the data retrieval.""" """The Class for handling the data retrieval."""
def __init__(self, host, count): def __init__(self, host, count):

View file

@ -111,11 +111,10 @@ class RachioControllerOnlineBinarySensor(RachioControllerBinarySensor):
if data[KEY_STATUS] == STATUS_ONLINE: if data[KEY_STATUS] == STATUS_ONLINE:
return True return True
elif data[KEY_STATUS] == STATUS_OFFLINE: if data[KEY_STATUS] == STATUS_OFFLINE:
return False return False
else: _LOGGER.warning('"%s" reported in unknown state "%s"', self.name,
_LOGGER.warning('"%s" reported in unknown state "%s"', self.name, data[KEY_STATUS])
data[KEY_STATUS])
def _handle_update(self, *args, **kwargs) -> None: def _handle_update(self, *args, **kwargs) -> None:
"""Handle an update to the state of this sensor.""" """Handle an update to the state of this sensor."""

View file

@ -67,6 +67,6 @@ class RainCloudBinarySensor(RainCloudEntity, BinarySensorDevice):
"""Return the icon of this device.""" """Return the icon of this device."""
if self._sensor_type == 'is_watering': if self._sensor_type == 'is_watering':
return 'mdi:water' if self.is_on else 'mdi:water-off' return 'mdi:water' if self.is_on else 'mdi:water-off'
elif self._sensor_type == 'status': if self._sensor_type == 'status':
return 'mdi:pipe' if self.is_on else 'mdi:pipe-disconnected' return 'mdi:pipe' if self.is_on else 'mdi:pipe-disconnected'
return ICON_MAP.get(self._sensor_type) return ICON_MAP.get(self._sensor_type)

View file

@ -8,7 +8,7 @@ import logging
import voluptuous as vol import voluptuous as vol
import homeassistant.components.rpi_gpio as rpi_gpio from homeassistant.components import rpi_gpio
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA) BinarySensorDevice, PLATFORM_SCHEMA)
from homeassistant.const import DEVICE_DEFAULT_NAME from homeassistant.const import DEVICE_DEFAULT_NAME

View file

@ -10,7 +10,7 @@ import voluptuous as vol
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
PLATFORM_SCHEMA, BinarySensorDevice) PLATFORM_SCHEMA, BinarySensorDevice)
import homeassistant.components.rpi_pfio as rpi_pfio from homeassistant.components import rpi_pfio
from homeassistant.const import CONF_NAME, DEVICE_DEFAULT_NAME from homeassistant.const import CONF_NAME, DEVICE_DEFAULT_NAME
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv

View 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)

View file

@ -63,7 +63,7 @@ class TapsAffSensor(BinarySensorDevice):
self.data.update() self.data.update()
class TapsAffData(object): class TapsAffData:
"""Class for handling the data retrieval for pins.""" """Class for handling the data retrieval for pins."""
def __init__(self, location): def __init__(self, location):

View file

@ -129,9 +129,9 @@ class ThresholdSensor(BinarySensorDevice):
if self._threshold_lower is not None and \ if self._threshold_lower is not None and \
self._threshold_upper is not None: self._threshold_upper is not None:
return TYPE_RANGE return TYPE_RANGE
elif self._threshold_lower is not None: if self._threshold_lower is not None:
return TYPE_LOWER return TYPE_LOWER
elif self._threshold_upper is not None: if self._threshold_upper is not None:
return TYPE_UPPER return TYPE_UPPER
@property @property

View file

@ -23,7 +23,7 @@ from homeassistant.helpers.entity import generate_entity_id
from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.event import async_track_state_change
from homeassistant.util import utcnow from homeassistant.util import utcnow
REQUIREMENTS = ['numpy==1.14.5'] REQUIREMENTS = ['numpy==1.15.0']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View file

@ -28,7 +28,7 @@ class VolvoSensor(VolvoEntity, BinarySensorDevice):
val = getattr(self.vehicle, self._attribute) val = getattr(self.vehicle, self._attribute)
if self._attribute == 'bulb_failures': if self._attribute == 'bulb_failures':
return bool(val) return bool(val)
elif self._attribute in ['doors', 'windows']: if self._attribute in ['doors', 'windows']:
return any([val[key] for key in val if 'Open' in key]) return any([val[key] for key in val if 'Open' in key])
return val != 'Normal' return val != 'Normal'

View file

@ -15,7 +15,7 @@ _LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices_callback, discovery_info=None): def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Register discovered WeMo binary sensors.""" """Register discovered WeMo binary sensors."""
import pywemo.discovery as discovery from pywemo import discovery
if discovery_info is not None: if discovery_info is not None:
location = discovery_info['ssdp_description'] location = discovery_info['ssdp_description']

View file

@ -135,7 +135,7 @@ class IsWorkdaySensor(BinarySensorDevice):
"""Check if given day is in the includes list.""" """Check if given day is in the includes list."""
if day in self._workdays: if day in self._workdays:
return True return True
elif 'holiday' in self._workdays and now in self._obj_holidays: if 'holiday' in self._workdays and now in self._obj_holidays:
return True return True
return False return False
@ -144,7 +144,7 @@ class IsWorkdaySensor(BinarySensorDevice):
"""Check if given day is in the excludes list.""" """Check if given day is in the excludes list."""
if day in self._excludes: if day in self._excludes:
return True return True
elif 'holiday' in self._excludes and now in self._obj_holidays: if 'holiday' in self._excludes and now in self._obj_holidays:
return True return True
return False return False

View file

@ -124,7 +124,7 @@ class XiaomiNatgasSensor(XiaomiBinarySensor):
return False return False
self._state = True self._state = True
return True return True
elif value == '0': if value == '0':
if self._state: if self._state:
self._state = False self._state = False
return True return True
@ -184,7 +184,7 @@ class XiaomiMotionSensor(XiaomiBinarySensor):
return False return False
self._state = True self._state = True
return True return True
elif value == NO_MOTION: if value == NO_MOTION:
if not self._state: if not self._state:
return False return False
self._state = False self._state = False
@ -224,7 +224,7 @@ class XiaomiDoorSensor(XiaomiBinarySensor):
return False return False
self._state = True self._state = True
return True return True
elif value == 'close': if value == 'close':
self._open_since = 0 self._open_since = 0
if self._state: if self._state:
self._state = False self._state = False
@ -254,7 +254,7 @@ class XiaomiWaterLeakSensor(XiaomiBinarySensor):
return False return False
self._state = True self._state = True
return True return True
elif value == 'no_leak': if value == 'no_leak':
if self._state: if self._state:
self._state = False self._state = False
return True return True
@ -290,7 +290,7 @@ class XiaomiSmokeSensor(XiaomiBinarySensor):
return False return False
self._state = True self._state = True
return True return True
elif value == '0': if value == '0':
if self._state: if self._state:
self._state = False self._state = False
return True return True

View file

@ -10,7 +10,7 @@ import homeassistant.util.dt as dt_util
from homeassistant.helpers.event import track_point_in_time from homeassistant.helpers.event import track_point_in_time
from homeassistant.components import zwave from homeassistant.components import zwave
from homeassistant.components.zwave import workaround from homeassistant.components.zwave import workaround
from homeassistant.components.zwave import async_setup_platform # noqa # pylint: disable=unused-import from homeassistant.components.zwave import async_setup_platform # noqa pylint: disable=unused-import
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
DOMAIN, DOMAIN,
BinarySensorDevice) BinarySensorDevice)

View file

@ -40,7 +40,7 @@ SNAP_PICTURE_SCHEMA = vol.Schema({
}) })
class BlinkSystem(object): class BlinkSystem:
"""Blink System class.""" """Blink System class."""
def __init__(self, config_info): def __init__(self, config_info):

View file

@ -50,7 +50,7 @@ def setup(hass, config):
return True return True
class BloomSky(object): class BloomSky:
"""Handle all communication with the BloomSky API.""" """Handle all communication with the BloomSky API."""
# API documentation at http://weatherlution.com/bloomsky-api/ # API documentation at http://weatherlution.com/bloomsky-api/

View file

@ -118,7 +118,7 @@ def setup_account(account_config: dict, hass, name: str) \
return cd_account return cd_account
class BMWConnectedDriveAccount(object): class BMWConnectedDriveAccount:
"""Representation of a BMW vehicle.""" """Representation of a BMW vehicle."""
def __init__(self, username: str, password: str, region_str: str, def __init__(self, username: str, password: str, region_str: str,

View file

@ -130,7 +130,7 @@ class CalendarEventDevice(Entity):
now = dt.now() now = dt.now()
if start <= now and end > now: if start <= now < end:
return STATE_ON return STATE_ON
if now >= end: if now >= end:

View file

@ -125,7 +125,7 @@ class WebDavCalendarEventDevice(CalendarEventDevice):
return await self.data.async_get_events(hass, start_date, end_date) return await self.data.async_get_events(hass, start_date, end_date)
class WebDavCalendarData(object): class WebDavCalendarData:
"""Class to utilize the calendar dav client object to get next event.""" """Class to utilize the calendar dav client object to get next event."""
def __init__(self, calendar, include_all_day, search): def __init__(self, calendar, include_all_day, search):

View file

@ -28,7 +28,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
]) ])
class DemoGoogleCalendarData(object): class DemoGoogleCalendarData:
"""Representation of a Demo Calendar element.""" """Representation of a Demo Calendar element."""
event = {} event = {}

View file

@ -55,7 +55,7 @@ class GoogleCalendarEventDevice(CalendarEventDevice):
return await self.data.async_get_events(hass, start_date, end_date) return await self.data.async_get_events(hass, start_date, end_date)
class GoogleCalendarData(object): class GoogleCalendarData:
"""Class to utilize calendar service object to get next event.""" """Class to utilize calendar service object to get next event."""
def __init__(self, calendar_service, calendar_id, search, def __init__(self, calendar_service, calendar_id, search,

View file

@ -26,6 +26,9 @@ CONF_PROJECT_DUE_DATE = 'due_date_days'
CONF_PROJECT_LABEL_WHITELIST = 'labels' CONF_PROJECT_LABEL_WHITELIST = 'labels'
CONF_PROJECT_WHITELIST = 'include_projects' CONF_PROJECT_WHITELIST = 'include_projects'
# https://github.com/PyCQA/pylint/pull/2320
# pylint: disable=fixme
# Calendar Platform: Does this calendar event last all day? # Calendar Platform: Does this calendar event last all day?
ALL_DAY = 'all_day' ALL_DAY = 'all_day'
# Attribute: All tasks in this project # Attribute: All tasks in this project
@ -280,7 +283,7 @@ class TodoistProjectDevice(CalendarEventDevice):
return attributes return attributes
class TodoistProjectData(object): class TodoistProjectData:
""" """
Class used by the Task Device service object to hold all Todoist Tasks. Class used by the Task Device service object to hold all Todoist Tasks.
@ -503,7 +506,7 @@ class TodoistProjectData(object):
time_format = '%a %d %b %Y %H:%M:%S %z' time_format = '%a %d %b %Y %H:%M:%S %z'
for task in project_task_data: for task in project_task_data:
due_date = datetime.strptime(task['due_date_utc'], time_format) due_date = datetime.strptime(task['due_date_utc'], time_format)
if due_date > start_date and due_date < end_date: if start_date < due_date < end_date:
event = { event = {
'uid': task['id'], 'uid': task['id'],
'title': task['content'], 'title': task['content'],

View file

@ -19,7 +19,8 @@ import async_timeout
import voluptuous as vol import voluptuous as vol
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.const import ATTR_ENTITY_ID from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, \
SERVICE_TURN_ON
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.loader import bind_hass from homeassistant.loader import bind_hass
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
@ -47,6 +48,9 @@ STATE_RECORDING = 'recording'
STATE_STREAMING = 'streaming' STATE_STREAMING = 'streaming'
STATE_IDLE = 'idle' STATE_IDLE = 'idle'
# Bitfield of features supported by the camera entity
SUPPORT_ON_OFF = 1
DEFAULT_CONTENT_TYPE = 'image/jpeg' DEFAULT_CONTENT_TYPE = 'image/jpeg'
ENTITY_IMAGE_URL = '/api/camera_proxy/{0}?token={1}' ENTITY_IMAGE_URL = '/api/camera_proxy/{0}?token={1}'
@ -79,6 +83,35 @@ class Image:
content = attr.ib(type=bytes) content = attr.ib(type=bytes)
@bind_hass
def turn_off(hass, entity_id=None):
"""Turn off camera."""
hass.add_job(async_turn_off, hass, entity_id)
@bind_hass
async def async_turn_off(hass, entity_id=None):
"""Turn off camera."""
data = {ATTR_ENTITY_ID: entity_id} if entity_id else {}
await hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, data)
@bind_hass
def turn_on(hass, entity_id=None):
"""Turn on camera."""
hass.add_job(async_turn_on, hass, entity_id)
@bind_hass
async def async_turn_on(hass, entity_id=None):
"""Turn on camera, and set operation mode."""
data = {}
if entity_id is not None:
data[ATTR_ENTITY_ID] = entity_id
await hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data)
@bind_hass @bind_hass
def enable_motion_detection(hass, entity_id=None): def enable_motion_detection(hass, entity_id=None):
"""Enable Motion Detection.""" """Enable Motion Detection."""
@ -119,6 +152,9 @@ async def async_get_image(hass, entity_id, timeout=10):
if camera is None: if camera is None:
raise HomeAssistantError('Camera not found') raise HomeAssistantError('Camera not found')
if not camera.is_on:
raise HomeAssistantError('Camera is off')
with suppress(asyncio.CancelledError, asyncio.TimeoutError): with suppress(asyncio.CancelledError, asyncio.TimeoutError):
with async_timeout.timeout(timeout, loop=hass.loop): with async_timeout.timeout(timeout, loop=hass.loop):
image = await camera.async_camera_image() image = await camera.async_camera_image()
@ -163,6 +199,12 @@ async def async_setup(hass, config):
await camera.async_enable_motion_detection() await camera.async_enable_motion_detection()
elif service.service == SERVICE_DISABLE_MOTION: elif service.service == SERVICE_DISABLE_MOTION:
await camera.async_disable_motion_detection() await camera.async_disable_motion_detection()
elif service.service == SERVICE_TURN_OFF and \
camera.supported_features & SUPPORT_ON_OFF:
await camera.async_turn_off()
elif service.service == SERVICE_TURN_ON and \
camera.supported_features & SUPPORT_ON_OFF:
await camera.async_turn_on()
if not camera.should_poll: if not camera.should_poll:
continue continue
@ -200,6 +242,12 @@ async def async_setup(hass, config):
except OSError as err: except OSError as err:
_LOGGER.error("Can't write image to file: %s", err) _LOGGER.error("Can't write image to file: %s", err)
hass.services.async_register(
DOMAIN, SERVICE_TURN_OFF, async_handle_camera_service,
schema=CAMERA_SERVICE_SCHEMA)
hass.services.async_register(
DOMAIN, SERVICE_TURN_ON, async_handle_camera_service,
schema=CAMERA_SERVICE_SCHEMA)
hass.services.async_register( hass.services.async_register(
DOMAIN, SERVICE_ENABLE_MOTION, async_handle_camera_service, DOMAIN, SERVICE_ENABLE_MOTION, async_handle_camera_service,
schema=CAMERA_SERVICE_SCHEMA) schema=CAMERA_SERVICE_SCHEMA)
@ -243,6 +291,11 @@ class Camera(Entity):
"""Return a link to the camera feed as entity picture.""" """Return a link to the camera feed as entity picture."""
return ENTITY_IMAGE_URL.format(self.entity_id, self.access_tokens[-1]) return ENTITY_IMAGE_URL.format(self.entity_id, self.access_tokens[-1])
@property
def supported_features(self):
"""Flag supported features."""
return 0
@property @property
def is_recording(self): def is_recording(self):
"""Return true if the device is recording.""" """Return true if the device is recording."""
@ -301,32 +354,23 @@ class Camera(Entity):
last_image = None last_image = None
try: while True:
while True: img_bytes = await self.async_camera_image()
img_bytes = await self.async_camera_image() if not img_bytes:
if not img_bytes: break
break
if img_bytes and img_bytes != last_image: if img_bytes and img_bytes != last_image:
await write_to_mjpeg_stream(img_bytes)
# Chrome seems to always ignore first picture,
# print it twice.
if last_image is None:
await write_to_mjpeg_stream(img_bytes) await write_to_mjpeg_stream(img_bytes)
last_image = img_bytes
# Chrome seems to always ignore first picture, await asyncio.sleep(interval)
# print it twice.
if last_image is None:
await write_to_mjpeg_stream(img_bytes)
last_image = img_bytes return response
await asyncio.sleep(interval)
except asyncio.CancelledError:
_LOGGER.debug("Stream closed by frontend.")
response = None
raise
finally:
if response is not None:
await response.write_eof()
async def handle_async_mjpeg_stream(self, request): async def handle_async_mjpeg_stream(self, request):
"""Serve an HTTP MJPEG stream from the camera. """Serve an HTTP MJPEG stream from the camera.
@ -342,14 +386,38 @@ class Camera(Entity):
"""Return the camera state.""" """Return the camera state."""
if self.is_recording: if self.is_recording:
return STATE_RECORDING return STATE_RECORDING
elif self.is_streaming: if self.is_streaming:
return STATE_STREAMING return STATE_STREAMING
return STATE_IDLE return STATE_IDLE
@property
def is_on(self):
"""Return true if on."""
return True
def turn_off(self):
"""Turn off camera."""
raise NotImplementedError()
@callback
def async_turn_off(self):
"""Turn off camera."""
return self.hass.async_add_job(self.turn_off)
def turn_on(self):
"""Turn off camera."""
raise NotImplementedError()
@callback
def async_turn_on(self):
"""Turn off camera."""
return self.hass.async_add_job(self.turn_on)
def enable_motion_detection(self): def enable_motion_detection(self):
"""Enable motion detection in the camera.""" """Enable motion detection in the camera."""
raise NotImplementedError() raise NotImplementedError()
@callback
def async_enable_motion_detection(self): def async_enable_motion_detection(self):
"""Call the job and enable motion detection.""" """Call the job and enable motion detection."""
return self.hass.async_add_job(self.enable_motion_detection) return self.hass.async_add_job(self.enable_motion_detection)
@ -358,6 +426,7 @@ class Camera(Entity):
"""Disable motion detection in camera.""" """Disable motion detection in camera."""
raise NotImplementedError() raise NotImplementedError()
@callback
def async_disable_motion_detection(self): def async_disable_motion_detection(self):
"""Call the job and disable motion detection.""" """Call the job and disable motion detection."""
return self.hass.async_add_job(self.disable_motion_detection) return self.hass.async_add_job(self.disable_motion_detection)
@ -402,17 +471,19 @@ class CameraView(HomeAssistantView):
camera = self.component.get_entity(entity_id) camera = self.component.get_entity(entity_id)
if camera is None: if camera is None:
status = 404 if request[KEY_AUTHENTICATED] else 401 raise web.HTTPNotFound()
return web.Response(status=status)
authenticated = (request[KEY_AUTHENTICATED] or authenticated = (request[KEY_AUTHENTICATED] or
request.query.get('token') in camera.access_tokens) request.query.get('token') in camera.access_tokens)
if not authenticated: if not authenticated:
return web.Response(status=401) raise web.HTTPUnauthorized()
response = await self.handle(request, camera) if not camera.is_on:
return response _LOGGER.debug('Camera is off.')
raise web.HTTPServiceUnavailable()
return await self.handle(request, camera)
async def handle(self, request, camera): async def handle(self, request, camera):
"""Handle the camera request.""" """Handle the camera request."""
@ -435,7 +506,7 @@ class CameraImageView(CameraView):
return web.Response(body=image, return web.Response(body=image,
content_type=camera.content_type) content_type=camera.content_type)
return web.Response(status=500) raise web.HTTPInternalServerError()
class CameraMjpegStream(CameraView): class CameraMjpegStream(CameraView):
@ -448,8 +519,7 @@ class CameraMjpegStream(CameraView):
"""Serve camera stream, possibly with interval.""" """Serve camera stream, possibly with interval."""
interval = request.query.get('interval') interval = request.query.get('interval')
if interval is None: if interval is None:
await camera.handle_async_mjpeg_stream(request) return await camera.handle_async_mjpeg_stream(request)
return
try: try:
# Compose camera stream from stills # Compose camera stream from stills
@ -457,10 +527,9 @@ class CameraMjpegStream(CameraView):
if interval < MIN_STREAM_INTERVAL: if interval < MIN_STREAM_INTERVAL:
raise ValueError("Stream interval must be be > {}" raise ValueError("Stream interval must be be > {}"
.format(MIN_STREAM_INTERVAL)) .format(MIN_STREAM_INTERVAL))
await camera.handle_async_still_stream(request, interval) return await camera.handle_async_still_stream(request, interval)
return
except ValueError: except ValueError:
return web.Response(status=400) raise web.HTTPBadRequest()
@callback @callback

View file

@ -64,7 +64,7 @@ class AmcrestCam(Camera):
yield from super().handle_async_mjpeg_stream(request) yield from super().handle_async_mjpeg_stream(request)
return return
elif self._stream_source == STREAM_SOURCE_LIST['mjpeg']: if self._stream_source == STREAM_SOURCE_LIST['mjpeg']:
# stream an MJPEG image stream directly from the camera # stream an MJPEG image stream directly from the camera
websession = async_get_clientsession(self.hass) websession = async_get_clientsession(self.hass)
streaming_url = self._camera.mjpeg_url(typeno=self._resolution) streaming_url = self._camera.mjpeg_url(typeno=self._resolution)

View file

@ -23,7 +23,7 @@ def _get_image_url(host, port, mode):
"""Set the URL to get the image.""" """Set the URL to get the image."""
if mode == 'mjpeg': if mode == 'mjpeg':
return 'http://{}:{}/axis-cgi/mjpg/video.cgi'.format(host, port) return 'http://{}:{}/axis-cgi/mjpg/video.cgi'.format(host, port)
elif mode == 'single': if mode == 'single':
return 'http://{}:{}/axis-cgi/jpg/image.cgi'.format(host, port) return 'http://{}:{}/axis-cgi/jpg/image.cgi'.format(host, port)

View file

@ -4,10 +4,10 @@ Demo camera platform that has a fake camera.
For more details about this platform, please refer to the documentation For more details about this platform, please refer to the documentation
https://home-assistant.io/components/demo/ https://home-assistant.io/components/demo/
""" """
import os
import logging import logging
import homeassistant.util.dt as dt_util import os
from homeassistant.components.camera import Camera
from homeassistant.components.camera import Camera, SUPPORT_ON_OFF
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -16,26 +16,29 @@ async def async_setup_platform(hass, config, async_add_devices,
discovery_info=None): discovery_info=None):
"""Set up the Demo camera platform.""" """Set up the Demo camera platform."""
async_add_devices([ async_add_devices([
DemoCamera(hass, config, 'Demo camera') DemoCamera('Demo camera')
]) ])
class DemoCamera(Camera): class DemoCamera(Camera):
"""The representation of a Demo camera.""" """The representation of a Demo camera."""
def __init__(self, hass, config, name): def __init__(self, name):
"""Initialize demo camera component.""" """Initialize demo camera component."""
super().__init__() super().__init__()
self._parent = hass
self._name = name self._name = name
self._motion_status = False self._motion_status = False
self.is_streaming = True
self._images_index = 0
def camera_image(self): def camera_image(self):
"""Return a faked still image response.""" """Return a faked still image response."""
now = dt_util.utcnow() self._images_index = (self._images_index + 1) % 4
image_path = os.path.join( image_path = os.path.join(
os.path.dirname(__file__), 'demo_{}.jpg'.format(now.second % 4)) os.path.dirname(__file__),
'demo_{}.jpg'.format(self._images_index))
_LOGGER.debug('Loading camera_image: %s', image_path)
with open(image_path, 'rb') as file: with open(image_path, 'rb') as file:
return file.read() return file.read()
@ -46,8 +49,21 @@ class DemoCamera(Camera):
@property @property
def should_poll(self): def should_poll(self):
"""Camera should poll periodically.""" """Demo camera doesn't need poll.
return True
Need explicitly call schedule_update_ha_state() after state changed.
"""
return False
@property
def supported_features(self):
"""Camera support turn on/off features."""
return SUPPORT_ON_OFF
@property
def is_on(self):
"""Whether camera is on (streaming)."""
return self.is_streaming
@property @property
def motion_detection_enabled(self): def motion_detection_enabled(self):
@ -57,7 +73,19 @@ class DemoCamera(Camera):
def enable_motion_detection(self): def enable_motion_detection(self):
"""Enable the Motion detection in base station (Arm).""" """Enable the Motion detection in base station (Arm)."""
self._motion_status = True self._motion_status = True
self.schedule_update_ha_state()
def disable_motion_detection(self): def disable_motion_detection(self):
"""Disable the motion detection in base station (Disarm).""" """Disable the motion detection in base station (Disarm)."""
self._motion_status = False self._motion_status = False
self.schedule_update_ha_state()
def turn_off(self):
"""Turn off camera."""
self.is_streaming = False
self.schedule_update_ha_state()
def turn_on(self):
"""Turn on camera."""
self.is_streaming = True
self.schedule_update_ha_state()

View file

@ -29,8 +29,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
}) })
@asyncio.coroutine async def async_setup_platform(hass, config, async_add_devices,
def async_setup_platform(hass, config, async_add_devices, discovery_info=None): discovery_info=None):
"""Set up a FFmpeg camera.""" """Set up a FFmpeg camera."""
if not hass.data[DATA_FFMPEG].async_run_test(config.get(CONF_INPUT)): if not hass.data[DATA_FFMPEG].async_run_test(config.get(CONF_INPUT)):
return return
@ -49,30 +49,30 @@ class FFmpegCamera(Camera):
self._input = config.get(CONF_INPUT) self._input = config.get(CONF_INPUT)
self._extra_arguments = config.get(CONF_EXTRA_ARGUMENTS) self._extra_arguments = config.get(CONF_EXTRA_ARGUMENTS)
@asyncio.coroutine async def async_camera_image(self):
def async_camera_image(self):
"""Return a still image response from the camera.""" """Return a still image response from the camera."""
from haffmpeg import ImageFrame, IMAGE_JPEG from haffmpeg import ImageFrame, IMAGE_JPEG
ffmpeg = ImageFrame(self._manager.binary, loop=self.hass.loop) ffmpeg = ImageFrame(self._manager.binary, loop=self.hass.loop)
image = yield from asyncio.shield(ffmpeg.get_image( image = await asyncio.shield(ffmpeg.get_image(
self._input, output_format=IMAGE_JPEG, self._input, output_format=IMAGE_JPEG,
extra_cmd=self._extra_arguments), loop=self.hass.loop) extra_cmd=self._extra_arguments), loop=self.hass.loop)
return image return image
@asyncio.coroutine async def handle_async_mjpeg_stream(self, request):
def handle_async_mjpeg_stream(self, request):
"""Generate an HTTP MJPEG stream from the camera.""" """Generate an HTTP MJPEG stream from the camera."""
from haffmpeg import CameraMjpeg from haffmpeg import CameraMjpeg
stream = CameraMjpeg(self._manager.binary, loop=self.hass.loop) stream = CameraMjpeg(self._manager.binary, loop=self.hass.loop)
yield from stream.open_camera( await stream.open_camera(
self._input, extra_cmd=self._extra_arguments) self._input, extra_cmd=self._extra_arguments)
yield from async_aiohttp_proxy_stream( try:
self.hass, request, stream, return await async_aiohttp_proxy_stream(
'multipart/x-mixed-replace;boundary=ffserver') self.hass, request, stream,
yield from stream.close() 'multipart/x-mixed-replace;boundary=ffserver')
finally:
await stream.close()
@property @property
def name(self): def name(self):

View file

@ -123,19 +123,18 @@ class MjpegCamera(Camera):
with closing(req) as response: with closing(req) as response:
return extract_image_from_mjpeg(response.iter_content(102400)) return extract_image_from_mjpeg(response.iter_content(102400))
@asyncio.coroutine async def handle_async_mjpeg_stream(self, request):
def handle_async_mjpeg_stream(self, request):
"""Generate an HTTP MJPEG stream from the camera.""" """Generate an HTTP MJPEG stream from the camera."""
# aiohttp don't support DigestAuth -> Fallback # aiohttp don't support DigestAuth -> Fallback
if self._authentication == HTTP_DIGEST_AUTHENTICATION: if self._authentication == HTTP_DIGEST_AUTHENTICATION:
yield from super().handle_async_mjpeg_stream(request) await super().handle_async_mjpeg_stream(request)
return return
# connect to stream # connect to stream
websession = async_get_clientsession(self.hass) websession = async_get_clientsession(self.hass)
stream_coro = websession.get(self._mjpeg_url, auth=self._auth) stream_coro = websession.get(self._mjpeg_url, auth=self._auth)
yield from async_aiohttp_proxy_web(self.hass, request, stream_coro) return await async_aiohttp_proxy_web(self.hass, request, stream_coro)
@property @property
def name(self): def name(self):

View file

@ -11,7 +11,7 @@ import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.core import callback from homeassistant.core import callback
import homeassistant.components.mqtt as mqtt from homeassistant.components import mqtt
from homeassistant.const import CONF_NAME from homeassistant.const import CONF_NAME
from homeassistant.components.camera import Camera, PLATFORM_SCHEMA from homeassistant.components.camera import Camera, PLATFORM_SCHEMA
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv

View file

@ -9,8 +9,9 @@ from datetime import timedelta
import requests import requests
import homeassistant.components.nest as nest from homeassistant.components import nest
from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera) from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera,
SUPPORT_ON_OFF)
from homeassistant.util.dt import utcnow from homeassistant.util.dt import utcnow
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -76,7 +77,36 @@ class NestCamera(Camera):
"""Return the brand of the camera.""" """Return the brand of the camera."""
return NEST_BRAND return NEST_BRAND
# This doesn't seem to be getting called regularly, for some reason @property
def supported_features(self):
"""Nest Cam support turn on and off."""
return SUPPORT_ON_OFF
@property
def is_on(self):
"""Return true if on."""
return self._online and self._is_streaming
def turn_off(self):
"""Turn off camera."""
_LOGGER.debug('Turn off camera %s', self._name)
# Calling Nest API in is_streaming setter.
# device.is_streaming would not immediately change until the process
# finished in Nest Cam.
self.device.is_streaming = False
def turn_on(self):
"""Turn on camera."""
if not self._online:
_LOGGER.error('Camera %s is offline.', self._name)
return
_LOGGER.debug('Turn on camera %s', self._name)
# Calling Nest API in is_streaming setter.
# device.is_streaming would not immediately change until the process
# finished in Nest Cam.
self.device.is_streaming = True
def update(self): def update(self):
"""Cache value from Python-nest.""" """Cache value from Python-nest."""
self._location = self.device.where self._location = self.device.where

View file

@ -105,6 +105,6 @@ class NetatmoCamera(Camera):
"""Return the camera model.""" """Return the camera model."""
if self._cameratype == "NOC": if self._cameratype == "NOC":
return "Presence" return "Presence"
elif self._cameratype == "NACamera": if self._cameratype == "NACamera":
return "Welcome" return "Welcome"
return None return None

View file

@ -2,56 +2,53 @@
Proxy camera platform that enables image processing of camera data. Proxy camera platform that enables image processing of camera data.
For more details about this platform, please refer to the documentation For more details about this platform, please refer to the documentation
https://home-assistant.io/components/proxy https://www.home-assistant.io/components/camera.proxy/
""" """
import logging
import asyncio import asyncio
import logging
import aiohttp import aiohttp
import async_timeout import async_timeout
import voluptuous as vol import voluptuous as vol
from homeassistant.util.async_ import run_coroutine_threadsafe from homeassistant.components.camera import PLATFORM_SCHEMA, Camera
from homeassistant.const import CONF_ENTITY_ID, CONF_NAME, HTTP_HEADER_HA_AUTH
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
import homeassistant.util.dt as dt_util
from homeassistant.const import (
CONF_NAME, CONF_ENTITY_ID, HTTP_HEADER_HA_AUTH)
from homeassistant.components.camera import (
PLATFORM_SCHEMA, Camera)
from homeassistant.helpers.aiohttp_client import ( from homeassistant.helpers.aiohttp_client import (
async_get_clientsession, async_aiohttp_proxy_web) async_aiohttp_proxy_web, async_get_clientsession)
from homeassistant.util.async_ import run_coroutine_threadsafe
import homeassistant.util.dt as dt_util
REQUIREMENTS = ['pillow==5.0.0'] REQUIREMENTS = ['pillow==5.2.0']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_MAX_IMAGE_WIDTH = "max_image_width" CONF_CACHE_IMAGES = 'cache_images'
CONF_IMAGE_QUALITY = "image_quality" CONF_FORCE_RESIZE = 'force_resize'
CONF_IMAGE_REFRESH_RATE = "image_refresh_rate" CONF_IMAGE_QUALITY = 'image_quality'
CONF_FORCE_RESIZE = "force_resize" CONF_IMAGE_REFRESH_RATE = 'image_refresh_rate'
CONF_MAX_STREAM_WIDTH = "max_stream_width" CONF_MAX_IMAGE_WIDTH = 'max_image_width'
CONF_STREAM_QUALITY = "stream_quality" CONF_MAX_STREAM_WIDTH = 'max_stream_width'
CONF_CACHE_IMAGES = "cache_images" CONF_STREAM_QUALITY = 'stream_quality'
DEFAULT_BASENAME = "Camera Proxy" DEFAULT_BASENAME = "Camera Proxy"
DEFAULT_QUALITY = 75 DEFAULT_QUALITY = 75
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_CACHE_IMAGES, False): cv.boolean,
vol.Optional(CONF_MAX_IMAGE_WIDTH): int, vol.Optional(CONF_FORCE_RESIZE, False): cv.boolean,
vol.Optional(CONF_IMAGE_QUALITY): int, vol.Optional(CONF_IMAGE_QUALITY): int,
vol.Optional(CONF_IMAGE_REFRESH_RATE): float, vol.Optional(CONF_IMAGE_REFRESH_RATE): float,
vol.Optional(CONF_FORCE_RESIZE, False): cv.boolean, vol.Optional(CONF_MAX_IMAGE_WIDTH): int,
vol.Optional(CONF_CACHE_IMAGES, False): cv.boolean,
vol.Optional(CONF_MAX_STREAM_WIDTH): int, vol.Optional(CONF_MAX_STREAM_WIDTH): int,
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_STREAM_QUALITY): int, vol.Optional(CONF_STREAM_QUALITY): int,
}) })
async def async_setup_platform(hass, config, async_add_devices, async def async_setup_platform(
discovery_info=None): hass, config, async_add_devices, discovery_info=None):
"""Set up the Proxy camera platform.""" """Set up the Proxy camera platform."""
async_add_devices([ProxyCamera(hass, config)]) async_add_devices([ProxyCamera(hass, config)])
@ -69,7 +66,7 @@ def _resize_image(image, opts):
img = Image.open(io.BytesIO(image)) img = Image.open(io.BytesIO(image))
imgfmt = str(img.format) imgfmt = str(img.format)
if imgfmt != 'PNG' and imgfmt != 'JPEG': if imgfmt not in ('PNG', 'JPEG'):
_LOGGER.debug("Image is of unsupported type: %s", imgfmt) _LOGGER.debug("Image is of unsupported type: %s", imgfmt)
return image return image
@ -77,7 +74,7 @@ def _resize_image(image, opts):
old_size = len(image) old_size = len(image)
if old_width <= new_width: if old_width <= new_width:
if opts.quality is None: if opts.quality is None:
_LOGGER.debug("Image is smaller-than / equal-to requested width") _LOGGER.debug("Image is smaller-than/equal-to requested width")
return image return image
new_width = old_width new_width = old_width
@ -86,7 +83,7 @@ def _resize_image(image, opts):
img = img.resize((new_width, new_height), Image.ANTIALIAS) img = img.resize((new_width, new_height), Image.ANTIALIAS)
imgbuf = io.BytesIO() imgbuf = io.BytesIO()
img.save(imgbuf, "JPEG", optimize=True, quality=quality) img.save(imgbuf, 'JPEG', optimize=True, quality=quality)
newimage = imgbuf.getvalue() newimage = imgbuf.getvalue()
if not opts.force_resize and len(newimage) >= old_size: if not opts.force_resize and len(newimage) >= old_size:
_LOGGER.debug("Using original image(%d bytes) " _LOGGER.debug("Using original image(%d bytes) "
@ -94,11 +91,9 @@ def _resize_image(image, opts):
old_size, len(newimage)) old_size, len(newimage))
return image return image
_LOGGER.debug("Resized image " _LOGGER.debug(
"from (%dx%d - %d bytes) " "Resized image from (%dx%d - %d bytes) to (%dx%d - %d bytes)",
"to (%dx%d - %d bytes)", old_width, old_height, old_size, new_width, new_height, len(newimage))
old_width, old_height, old_size,
new_width, new_height, len(newimage))
return newimage return newimage
@ -112,7 +107,7 @@ class ImageOpts():
self.force_resize = force_resize self.force_resize = force_resize
def __bool__(self): def __bool__(self):
"""Bool evalution rules.""" """Bool evaluation rules."""
return bool(self.max_width or self.quality) return bool(self.max_width or self.quality)
@ -133,8 +128,7 @@ class ProxyCamera(Camera):
config.get(CONF_FORCE_RESIZE)) config.get(CONF_FORCE_RESIZE))
self._stream_opts = ImageOpts( self._stream_opts = ImageOpts(
config.get(CONF_MAX_STREAM_WIDTH), config.get(CONF_MAX_STREAM_WIDTH), config.get(CONF_STREAM_QUALITY),
config.get(CONF_STREAM_QUALITY),
True) True)
self._image_refresh_rate = config.get(CONF_IMAGE_REFRESH_RATE) self._image_refresh_rate = config.get(CONF_IMAGE_REFRESH_RATE)
@ -145,8 +139,7 @@ class ProxyCamera(Camera):
self._last_image = None self._last_image = None
self._headers = ( self._headers = (
{HTTP_HEADER_HA_AUTH: self.hass.config.api.api_password} {HTTP_HEADER_HA_AUTH: self.hass.config.api.api_password}
if self.hass.config.api.api_password is not None if self.hass.config.api.api_password is not None else None)
else None)
def camera_image(self): def camera_image(self):
"""Return camera image.""" """Return camera image."""
@ -191,12 +184,12 @@ class ProxyCamera(Camera):
stream_coro = websession.get(url, headers=self._headers) stream_coro = websession.get(url, headers=self._headers)
if not self._stream_opts: if not self._stream_opts:
await async_aiohttp_proxy_web(self.hass, request, stream_coro) return await async_aiohttp_proxy_web(
return self.hass, request, stream_coro)
response = aiohttp.web.StreamResponse() response = aiohttp.web.StreamResponse()
response.content_type = ('multipart/x-mixed-replace; ' response.content_type = (
'boundary=--frameboundary') 'multipart/x-mixed-replace; boundary=--frameboundary')
await response.prepare(request) await response.prepare(request)
async def write(img_bytes): async def write(img_bytes):
@ -229,15 +222,10 @@ class ProxyCamera(Camera):
_resize_image, image, self._stream_opts) _resize_image, image, self._stream_opts)
await write(image) await write(image)
data = data[jpg_end + 2:] data = data[jpg_end + 2:]
except asyncio.CancelledError:
_LOGGER.debug("Stream closed by frontend.")
req.close()
response = None
raise
finally: finally:
if response is not None: req.close()
await response.write_eof()
return response
@property @property
def name(self): def name(self):

View file

@ -1,5 +1,19 @@
# Describes the format for available camera services # Describes the format for available camera services
turn_off:
description: Turn off camera.
fields:
entity_id:
description: Entity id.
example: 'camera.living_room'
turn_on:
description: Turn on camera.
fields:
entity_id:
description: Entity id.
example: 'camera.living_room'
enable_motion_detection: enable_motion_detection:
description: Enable the motion detection in a camera. description: Enable the motion detection in a camera.
fields: fields:

View file

@ -171,10 +171,9 @@ class UnifiVideoCamera(Camera):
if retry: if retry:
self._login() self._login()
return _get_image(retry=False) return _get_image(retry=False)
else: _LOGGER.error(
_LOGGER.error( "Unable to log into camera, unable to get snapshot")
"Unable to log into camera, unable to get snapshot") raise
raise
return _get_image() return _get_image()

View file

@ -66,8 +66,7 @@ class VerisureSmartcam(Camera):
if not image_ids: if not image_ids:
return return
new_image_id = image_ids[0] new_image_id = image_ids[0]
if (new_image_id == '-1' or if new_image_id in ('-1', self._image_id):
self._image_id == new_image_id):
_LOGGER.debug("The image is the same, or loading image_id") _LOGGER.debug("The image is the same, or loading image_id")
return return
_LOGGER.debug("Download new image %s", new_image_id) _LOGGER.debug("Download new image %s", new_image_id)

View file

@ -12,7 +12,7 @@ from homeassistant.const import CONF_NAME
from homeassistant.components.camera.mjpeg import ( from homeassistant.components.camera.mjpeg import (
CONF_MJPEG_URL, CONF_STILL_IMAGE_URL, MjpegCamera) CONF_MJPEG_URL, CONF_STILL_IMAGE_URL, MjpegCamera)
import homeassistant.components.zoneminder as zoneminder from homeassistant.components import zoneminder
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View file

@ -65,7 +65,7 @@ def setup(hass, config):
return True return True
class CanaryData(object): class CanaryData:
"""Get the latest data and update the states.""" """Get the latest data and update the states."""
def __init__(self, username, password, timeout): def __init__(self, username, password, timeout):

View file

@ -22,7 +22,7 @@ async def async_setup(hass, config):
async def async_setup_entry(hass, entry): async def async_setup_entry(hass, entry):
"""Set up Cast from a config entry.""" """Set up Cast from a config entry."""
hass.async_add_job(hass.config_entries.async_forward_entry_setup( hass.async_create_task(hass.config_entries.async_forward_entry_setup(
entry, 'media_player')) entry, 'media_player'))
return True return True
@ -31,7 +31,7 @@ async def _async_has_devices(hass):
"""Return if there are devices that can be discovered.""" """Return if there are devices that can be discovered."""
from pychromecast.discovery import discover_chromecasts from pychromecast.discovery import discover_chromecasts
return await hass.async_add_job(discover_chromecasts) return await hass.async_add_executor_job(discover_chromecasts)
config_entry_flow.register_discovery_flow( config_entry_flow.register_discovery_flow(

View file

@ -145,7 +145,7 @@ class DaikinClimate(ClimateDevice):
if value is None: if value is None:
_LOGGER.error("Invalid value requested for key %s", key) _LOGGER.error("Invalid value requested for key %s", key)
else: else:
if value == "-" or value == "--": if value in ("-", "--"):
value = None value = None
elif cast_to_float: elif cast_to_float:
try: try:

View file

@ -177,7 +177,7 @@ class Thermostat(ClimateDevice):
return None return None
if self.current_operation == STATE_HEAT: if self.current_operation == STATE_HEAT:
return self.thermostat['runtime']['desiredHeat'] / 10.0 return self.thermostat['runtime']['desiredHeat'] / 10.0
elif self.current_operation == STATE_COOL: if self.current_operation == STATE_COOL:
return self.thermostat['runtime']['desiredCool'] / 10.0 return self.thermostat['runtime']['desiredCool'] / 10.0
return None return None
@ -217,15 +217,15 @@ class Thermostat(ClimateDevice):
return 'away' return 'away'
# A permanent hold from away climate # A permanent hold from away climate
return AWAY_MODE return AWAY_MODE
elif event['holdClimateRef'] != "": if event['holdClimateRef'] != "":
# Any other hold based on climate # Any other hold based on climate
return event['holdClimateRef'] return event['holdClimateRef']
# Any hold not based on a climate is a temp hold # Any hold not based on a climate is a temp hold
return TEMPERATURE_HOLD return TEMPERATURE_HOLD
elif event['type'].startswith('auto'): if event['type'].startswith('auto'):
# All auto modes are treated as holds # All auto modes are treated as holds
return event['type'][4:].lower() return event['type'][4:].lower()
elif event['type'] == 'vacation': if event['type'] == 'vacation':
self.vacation = event['name'] self.vacation = event['name']
return VACATION_HOLD return VACATION_HOLD
return None return None
@ -317,7 +317,7 @@ class Thermostat(ClimateDevice):
if hold == hold_mode: if hold == hold_mode:
# no change, so no action required # no change, so no action required
return return
elif hold_mode == 'None' or hold_mode is None: if hold_mode == 'None' or hold_mode is None:
if hold == VACATION_HOLD: if hold == VACATION_HOLD:
self.data.ecobee.delete_vacation( self.data.ecobee.delete_vacation(
self.thermostat_index, self.vacation) self.thermostat_index, self.vacation)

View file

@ -20,7 +20,7 @@ from homeassistant.const import (
from homeassistant.components.climate import ( from homeassistant.components.climate import (
ClimateDevice, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE, ClimateDevice, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE,
SUPPORT_FAN_MODE) SUPPORT_FAN_MODE)
import homeassistant.components.modbus as modbus from homeassistant.components import modbus
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['pyflexit==0.3'] REQUIREMENTS = ['pyflexit==0.3']

View file

@ -50,7 +50,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
HeatmiserV3Thermostat( HeatmiserV3Thermostat(
heatmiser, tstat.get(CONF_ID), tstat.get(CONF_NAME), serport) heatmiser, tstat.get(CONF_ID), tstat.get(CONF_NAME), serport)
]) ])
return
class HeatmiserV3Thermostat(ClimateDevice): class HeatmiserV3Thermostat(ClimateDevice):

View file

@ -87,7 +87,7 @@ class HMThermostat(HMDevice, ClimateDevice):
# HM ip etrv 2 uses the set_point_mode to say if its # HM ip etrv 2 uses the set_point_mode to say if its
# auto or manual # auto or manual
elif not set_point_mode == -1: if not set_point_mode == -1:
code = set_point_mode code = set_point_mode
# Other devices use the control_mode # Other devices use the control_mode
else: else:

View file

@ -165,7 +165,7 @@ class RoundThermostat(ClimateDevice):
self.client.set_temperature(self._name, temperature) self.client.set_temperature(self._name, temperature)
@property @property
def current_operation(self: ClimateDevice) -> str: def current_operation(self) -> str:
"""Get the current operation of the system.""" """Get the current operation of the system."""
return getattr(self.client, ATTR_SYSTEM_MODE, None) return getattr(self.client, ATTR_SYSTEM_MODE, None)
@ -174,7 +174,7 @@ class RoundThermostat(ClimateDevice):
"""Return true if away mode is on.""" """Return true if away mode is on."""
return self._away return self._away
def set_operation_mode(self: ClimateDevice, operation_mode: str) -> None: def set_operation_mode(self, operation_mode: str) -> None:
"""Set the HVAC mode for the thermostat.""" """Set the HVAC mode for the thermostat."""
if hasattr(self.client, ATTR_SYSTEM_MODE): if hasattr(self.client, ATTR_SYSTEM_MODE):
self.client.system_mode = operation_mode self.client.system_mode = operation_mode
@ -280,7 +280,7 @@ class HoneywellUSThermostat(ClimateDevice):
return self._device.setpoint_heat return self._device.setpoint_heat
@property @property
def current_operation(self: ClimateDevice) -> str: def current_operation(self) -> str:
"""Return current operation ie. heat, cool, idle.""" """Return current operation ie. heat, cool, idle."""
oper = getattr(self._device, ATTR_CURRENT_OPERATION, None) oper = getattr(self._device, ATTR_CURRENT_OPERATION, None)
if oper == "off": if oper == "off":
@ -373,7 +373,7 @@ class HoneywellUSThermostat(ClimateDevice):
except somecomfort.SomeComfortError: except somecomfort.SomeComfortError:
_LOGGER.error('Can not stop hold mode') _LOGGER.error('Can not stop hold mode')
def set_operation_mode(self: ClimateDevice, operation_mode: str) -> None: def set_operation_mode(self, operation_mode: str) -> None:
"""Set the system mode (Cool, Heat, etc).""" """Set the system mode (Cool, Heat, etc)."""
if hasattr(self._device, ATTR_SYSTEM_MODE): if hasattr(self._device, ATTR_SYSTEM_MODE):
self._device.system_mode = operation_mode self._device.system_mode = operation_mode

View file

@ -192,9 +192,9 @@ class MelissaClimate(ClimateDevice):
"""Translate Melissa states to hass states.""" """Translate Melissa states to hass states."""
if state == self._api.STATE_ON: if state == self._api.STATE_ON:
return STATE_ON return STATE_ON
elif state == self._api.STATE_OFF: if state == self._api.STATE_OFF:
return STATE_OFF return STATE_OFF
elif state == self._api.STATE_IDLE: if state == self._api.STATE_IDLE:
return STATE_IDLE return STATE_IDLE
return None return None
@ -202,11 +202,11 @@ class MelissaClimate(ClimateDevice):
"""Translate Melissa modes to hass states.""" """Translate Melissa modes to hass states."""
if mode == self._api.MODE_HEAT: if mode == self._api.MODE_HEAT:
return STATE_HEAT return STATE_HEAT
elif mode == self._api.MODE_COOL: if mode == self._api.MODE_COOL:
return STATE_COOL return STATE_COOL
elif mode == self._api.MODE_DRY: if mode == self._api.MODE_DRY:
return STATE_DRY return STATE_DRY
elif mode == self._api.MODE_FAN: if mode == self._api.MODE_FAN:
return STATE_FAN_ONLY return STATE_FAN_ONLY
_LOGGER.warning( _LOGGER.warning(
"Operation mode %s could not be mapped to hass", mode) "Operation mode %s could not be mapped to hass", mode)
@ -216,11 +216,11 @@ class MelissaClimate(ClimateDevice):
"""Translate Melissa fan modes to hass modes.""" """Translate Melissa fan modes to hass modes."""
if fan == self._api.FAN_AUTO: if fan == self._api.FAN_AUTO:
return STATE_AUTO return STATE_AUTO
elif fan == self._api.FAN_LOW: if fan == self._api.FAN_LOW:
return SPEED_LOW return SPEED_LOW
elif fan == self._api.FAN_MEDIUM: if fan == self._api.FAN_MEDIUM:
return SPEED_MEDIUM return SPEED_MEDIUM
elif fan == self._api.FAN_HIGH: if fan == self._api.FAN_HIGH:
return SPEED_HIGH return SPEED_HIGH
_LOGGER.warning("Fan mode %s could not be mapped to hass", fan) _LOGGER.warning("Fan mode %s could not be mapped to hass", fan)
return None return None
@ -229,24 +229,22 @@ class MelissaClimate(ClimateDevice):
"""Translate hass states to melissa modes.""" """Translate hass states to melissa modes."""
if mode == STATE_HEAT: if mode == STATE_HEAT:
return self._api.MODE_HEAT return self._api.MODE_HEAT
elif mode == STATE_COOL: if mode == STATE_COOL:
return self._api.MODE_COOL return self._api.MODE_COOL
elif mode == STATE_DRY: if mode == STATE_DRY:
return self._api.MODE_DRY return self._api.MODE_DRY
elif mode == STATE_FAN_ONLY: if mode == STATE_FAN_ONLY:
return self._api.MODE_FAN return self._api.MODE_FAN
else: _LOGGER.warning("Melissa have no setting for %s mode", mode)
_LOGGER.warning("Melissa have no setting for %s mode", mode)
def hass_fan_to_melissa(self, fan): def hass_fan_to_melissa(self, fan):
"""Translate hass fan modes to melissa modes.""" """Translate hass fan modes to melissa modes."""
if fan == STATE_AUTO: if fan == STATE_AUTO:
return self._api.FAN_AUTO return self._api.FAN_AUTO
elif fan == SPEED_LOW: if fan == SPEED_LOW:
return self._api.FAN_LOW return self._api.FAN_LOW
elif fan == SPEED_MEDIUM: if fan == SPEED_MEDIUM:
return self._api.FAN_MEDIUM return self._api.FAN_MEDIUM
elif fan == SPEED_HIGH: if fan == SPEED_HIGH:
return self._api.FAN_HIGH return self._api.FAN_HIGH
else: _LOGGER.warning("Melissa have no setting for %s fan mode", fan)
_LOGGER.warning("Melissa have no setting for %s fan mode", fan)

View file

@ -18,7 +18,7 @@ from homeassistant.const import (
from homeassistant.components.climate import ( from homeassistant.components.climate import (
ClimateDevice, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE) ClimateDevice, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE)
import homeassistant.components.modbus as modbus from homeassistant.components import modbus
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
DEPENDENCIES = ['modbus'] DEPENDENCIES = ['modbus']

View file

@ -10,7 +10,7 @@ import logging
import voluptuous as vol import voluptuous as vol
from homeassistant.core import callback from homeassistant.core import callback
import homeassistant.components.mqtt as mqtt from homeassistant.components import mqtt
from homeassistant.components.climate import ( from homeassistant.components.climate import (
STATE_HEAT, STATE_COOL, STATE_DRY, STATE_FAN_ONLY, ClimateDevice, STATE_HEAT, STATE_COOL, STATE_DRY, STATE_FAN_ONLY, ClimateDevice,

View file

@ -147,7 +147,7 @@ class NestThermostat(ClimateDevice):
"""Return current operation ie. heat, cool, idle.""" """Return current operation ie. heat, cool, idle."""
if self._mode in [STATE_HEAT, STATE_COOL, STATE_OFF, STATE_ECO]: if self._mode in [STATE_HEAT, STATE_COOL, STATE_OFF, STATE_ECO]:
return self._mode return self._mode
elif self._mode == NEST_MODE_HEAT_COOL: if self._mode == NEST_MODE_HEAT_COOL:
return STATE_AUTO return STATE_AUTO
return STATE_UNKNOWN return STATE_UNKNOWN

View file

@ -99,7 +99,7 @@ class NetatmoThermostat(ClimateDevice):
state = self._data.thermostatdata.relay_cmd state = self._data.thermostatdata.relay_cmd
if state == 0: if state == 0:
return STATE_IDLE return STATE_IDLE
elif state == 100: if state == 100:
return STATE_HEAT return STATE_HEAT
@property @property
@ -140,7 +140,7 @@ class NetatmoThermostat(ClimateDevice):
self._away = self._data.setpoint_mode == 'away' self._away = self._data.setpoint_mode == 'away'
class ThermostatData(object): class ThermostatData:
"""Get the latest data from Netatmo.""" """Get the latest data from Netatmo."""
def __init__(self, auth, device=None): def __init__(self, auth, device=None):

Some files were not shown because too many files have changed in this diff Show more