Merge pull request #20354 from home-assistant/rc

0.86.0
This commit is contained in:
Paulus Schoutsen 2019-01-23 12:49:23 -08:00 committed by GitHub
commit e049b35413
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
476 changed files with 9862 additions and 3739 deletions

View File

@ -122,12 +122,17 @@ omit =
homeassistant/components/*/ecovacs.py homeassistant/components/*/ecovacs.py
homeassistant/components/esphome/__init__.py homeassistant/components/esphome/__init__.py
homeassistant/components/*/esphome.py homeassistant/components/esphome/binary_sensor.py
homeassistant/components/esphome/cover.py
homeassistant/components/esphome/fan.py
homeassistant/components/esphome/light.py
homeassistant/components/esphome/sensor.py
homeassistant/components/esphome/switch.py
homeassistant/components/eufy.py homeassistant/components/eufy.py
homeassistant/components/*/eufy.py homeassistant/components/*/eufy.py
homeassistant/components/fibaro.py homeassistant/components/fibaro/__init__.py
homeassistant/components/*/fibaro.py homeassistant/components/*/fibaro.py
homeassistant/components/gc100.py homeassistant/components/gc100.py
@ -234,7 +239,7 @@ omit =
homeassistant/components/lutron_caseta.py homeassistant/components/lutron_caseta.py
homeassistant/components/*/lutron_caseta.py homeassistant/components/*/lutron_caseta.py
homeassistant/components/*/mailgun.py homeassistant/components/mailgun/notify.py
homeassistant/components/matrix.py homeassistant/components/matrix.py
homeassistant/components/*/matrix.py homeassistant/components/*/matrix.py
@ -276,7 +281,8 @@ omit =
homeassistant/components/*/opentherm_gw.py homeassistant/components/*/opentherm_gw.py
homeassistant/components/openuv/__init__.py homeassistant/components/openuv/__init__.py
homeassistant/components/*/openuv.py homeassistant/components/openuv/binary_sensor.py
homeassistant/components/openuv/sensor.py
homeassistant/components/plum_lightpad.py homeassistant/components/plum_lightpad.py
homeassistant/components/*/plum_lightpad.py homeassistant/components/*/plum_lightpad.py
@ -298,7 +304,9 @@ omit =
homeassistant/components/*/raincloud.py homeassistant/components/*/raincloud.py
homeassistant/components/rainmachine/__init__.py homeassistant/components/rainmachine/__init__.py
homeassistant/components/*/rainmachine.py homeassistant/components/rainmachine/binary_sensor.py
homeassistant/components/rainmachine/sensor.py
homeassistant/components/rainmachine/switch.py
homeassistant/components/raspihats.py homeassistant/components/raspihats.py
homeassistant/components/*/raspihats.py homeassistant/components/*/raspihats.py
@ -308,6 +316,9 @@ omit =
homeassistant/components/rfxtrx.py homeassistant/components/rfxtrx.py
homeassistant/components/*/rfxtrx.py homeassistant/components/*/rfxtrx.py
homeassistant/components/roku.py
homeassistant/components/*/roku.py
homeassistant/components/rpi_gpio.py homeassistant/components/rpi_gpio.py
homeassistant/components/*/rpi_gpio.py homeassistant/components/*/rpi_gpio.py
@ -327,7 +338,7 @@ omit =
homeassistant/components/*/sense.py homeassistant/components/*/sense.py
homeassistant/components/simplisafe/__init__.py homeassistant/components/simplisafe/__init__.py
homeassistant/components/*/simplisafe.py homeassistant/components/simplisafe/alarm_control_panel.py
homeassistant/components/sisyphus.py homeassistant/components/sisyphus.py
homeassistant/components/*/sisyphus.py homeassistant/components/*/sisyphus.py
@ -424,8 +435,14 @@ omit =
homeassistant/components/*/zabbix.py homeassistant/components/*/zabbix.py
homeassistant/components/zha/__init__.py homeassistant/components/zha/__init__.py
homeassistant/components/zha/binary_sensor.py
homeassistant/components/zha/const.py homeassistant/components/zha/const.py
homeassistant/components/zha/event.py homeassistant/components/zha/event.py
homeassistant/components/zha/fan.py
homeassistant/components/zha/light.py
homeassistant/components/zha/sensor.py
homeassistant/components/zha/switch.py
homeassistant/components/zha/api.py
homeassistant/components/zha/entities/* homeassistant/components/zha/entities/*
homeassistant/components/zha/helpers.py homeassistant/components/zha/helpers.py
homeassistant/components/*/zha.py homeassistant/components/*/zha.py
@ -519,7 +536,6 @@ omit =
homeassistant/components/device_tracker/fritz.py homeassistant/components/device_tracker/fritz.py
homeassistant/components/device_tracker/google_maps.py homeassistant/components/device_tracker/google_maps.py
homeassistant/components/device_tracker/googlehome.py homeassistant/components/device_tracker/googlehome.py
homeassistant/components/device_tracker/gpslogger.py
homeassistant/components/device_tracker/hitron_coda.py homeassistant/components/device_tracker/hitron_coda.py
homeassistant/components/device_tracker/huawei_router.py homeassistant/components/device_tracker/huawei_router.py
homeassistant/components/device_tracker/icloud.py homeassistant/components/device_tracker/icloud.py
@ -637,7 +653,6 @@ omit =
homeassistant/components/media_player/pioneer.py homeassistant/components/media_player/pioneer.py
homeassistant/components/media_player/pjlink.py homeassistant/components/media_player/pjlink.py
homeassistant/components/media_player/plex.py homeassistant/components/media_player/plex.py
homeassistant/components/media_player/roku.py
homeassistant/components/media_player/russound_rio.py homeassistant/components/media_player/russound_rio.py
homeassistant/components/media_player/russound_rnet.py homeassistant/components/media_player/russound_rnet.py
homeassistant/components/media_player/snapcast.py homeassistant/components/media_player/snapcast.py

1
.gitignore vendored
View File

@ -78,6 +78,7 @@ venv
.venv .venv
Pipfile* Pipfile*
share/* share/*
Scripts/
# vimmy stuff # vimmy stuff
*.swp *.swp

View File

@ -1,2 +0,0 @@
[settings]
multi_line_output=4

View File

@ -185,7 +185,6 @@ homeassistant/components/edp_redy.py @abmantis
homeassistant/components/eight_sleep.py @mezz64 homeassistant/components/eight_sleep.py @mezz64
homeassistant/components/*/eight_sleep.py @mezz64 homeassistant/components/*/eight_sleep.py @mezz64
homeassistant/components/esphome/*.py @OttoWinter homeassistant/components/esphome/*.py @OttoWinter
homeassistant/components/*/esphome.py @OttoWinter
# H # H
homeassistant/components/hive.py @Rendili @KJonline homeassistant/components/hive.py @Rendili @KJonline
@ -219,7 +218,6 @@ homeassistant/components/*/ness_alarm.py @nickw444
# O # O
homeassistant/components/openuv/* @bachya homeassistant/components/openuv/* @bachya
homeassistant/components/*/openuv.py @bachya
# P # P
homeassistant/components/point/* @fredrike homeassistant/components/point/* @fredrike
@ -231,13 +229,11 @@ homeassistant/components/*/qwikswitch.py @kellerza
# R # R
homeassistant/components/rainmachine/* @bachya homeassistant/components/rainmachine/* @bachya
homeassistant/components/*/rainmachine.py @bachya
homeassistant/components/*/random.py @fabaff homeassistant/components/*/random.py @fabaff
homeassistant/components/*/rfxtrx.py @danielhiversen homeassistant/components/*/rfxtrx.py @danielhiversen
# S # S
homeassistant/components/simplisafe/* @bachya homeassistant/components/simplisafe/* @bachya
homeassistant/components/*/simplisafe.py @bachya
# T # T
homeassistant/components/tahoma.py @philklei homeassistant/components/tahoma.py @philklei

View File

@ -16,7 +16,6 @@ LABEL maintainer="Paulus Schoutsen <Paulus@PaulusSchoutsen.nl>"
VOLUME /config VOLUME /config
RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app WORKDIR /usr/src/app
# Copy build scripts # Copy build scripts

View File

@ -1,6 +1,7 @@
"""Home Assistant auth provider.""" """Home Assistant auth provider."""
import base64 import base64
from collections import OrderedDict from collections import OrderedDict
import logging
from typing import Any, Dict, List, Optional, cast from typing import Any, Dict, List, Optional, cast
import bcrypt import bcrypt
@ -51,6 +52,15 @@ class Data:
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY, self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY,
private=True) private=True)
self._data = None # type: Optional[Dict[str, Any]] self._data = None # type: Optional[Dict[str, Any]]
self.is_legacy = False
@callback
def normalize_username(self, username: str) -> str:
"""Normalize a username based on the mode."""
if self.is_legacy:
return username
return username.strip()
async def async_load(self) -> None: async def async_load(self) -> None:
"""Load stored data.""" """Load stored data."""
@ -61,6 +71,20 @@ class Data:
'users': [] 'users': []
} }
for user in data['users']:
username = user['username']
# check if we have unstripped usernames
if username != username.strip():
self.is_legacy = True
logging.getLogger(__name__).warning(
"Home Assistant auth provider is running in legacy mode "
"because we detected usernames that start or end in a "
"space. Please change the username.")
break
self._data = data self._data = data
@property @property
@ -73,6 +97,7 @@ class Data:
Raises InvalidAuth if auth invalid. Raises InvalidAuth if auth invalid.
""" """
username = self.normalize_username(username)
dummy = b'$2b$12$CiuFGszHx9eNHxPuQcwBWez4CwDTOcLTX5CbOpV6gef2nYuXkY7BO' dummy = b'$2b$12$CiuFGszHx9eNHxPuQcwBWez4CwDTOcLTX5CbOpV6gef2nYuXkY7BO'
found = None found = None
@ -105,7 +130,10 @@ class Data:
def add_auth(self, username: str, password: str) -> None: 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): username = self.normalize_username(username)
if any(self.normalize_username(user['username']) == username
for user in self.users):
raise InvalidUser raise InvalidUser
self.users.append({ self.users.append({
@ -116,9 +144,11 @@ class Data:
@callback @callback
def async_remove_auth(self, username: str) -> None: def async_remove_auth(self, username: str) -> None:
"""Remove authentication.""" """Remove authentication."""
username = self.normalize_username(username)
index = None index = None
for i, user in enumerate(self.users): for i, user in enumerate(self.users):
if user['username'] == username: if self.normalize_username(user['username']) == username:
index = i index = i
break break
@ -132,8 +162,10 @@ class Data:
Raises InvalidUser if user cannot be found. Raises InvalidUser if user cannot be found.
""" """
username = self.normalize_username(username)
for user in self.users: for user in self.users:
if user['username'] == username: if self.normalize_username(user['username']) == username:
user['password'] = self.hash_password( user['password'] = self.hash_password(
new_password, True).decode() new_password, True).decode()
break break
@ -178,10 +210,15 @@ class HassAuthProvider(AuthProvider):
async def async_get_or_create_credentials( async def async_get_or_create_credentials(
self, flow_result: Dict[str, str]) -> Credentials: self, flow_result: Dict[str, str]) -> Credentials:
"""Get credentials based on the flow result.""" """Get credentials based on the flow result."""
username = flow_result['username'] if self.data is None:
await self.async_initialize()
assert self.data is not None
norm_username = self.data.normalize_username
username = norm_username(flow_result['username'])
for credential in await self.async_credentials(): for credential in await self.async_credentials():
if credential.data['username'] == username: if norm_username(credential.data['username']) == username:
return credential return credential
# Create new credentials. # Create new credentials.

View File

@ -18,7 +18,7 @@ from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import discovery from homeassistant.helpers import discovery
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['abodepy==0.14.0'] REQUIREMENTS = ['abodepy==0.15.0']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -46,8 +46,8 @@ CONFIG_SCHEMA = vol.Schema({
SCHEMA_SERVICE_WRITE_DATA_BY_NAME = vol.Schema({ SCHEMA_SERVICE_WRITE_DATA_BY_NAME = vol.Schema({
vol.Required(CONF_ADS_TYPE): vol.Required(CONF_ADS_TYPE):
vol.In([ADSTYPE_INT, ADSTYPE_UINT, ADSTYPE_BYTE]), vol.In([ADSTYPE_INT, ADSTYPE_UINT, ADSTYPE_BYTE, ADSTYPE_BOOL]),
vol.Required(CONF_ADS_VALUE): cv.match_all, vol.Required(CONF_ADS_VALUE): vol.Coerce(int),
vol.Required(CONF_ADS_VAR): cv.string, vol.Required(CONF_ADS_VAR): cv.string,
}) })

View File

@ -21,6 +21,8 @@ from homeassistant.helpers.entity_component import EntityComponent
DOMAIN = 'alarm_control_panel' DOMAIN = 'alarm_control_panel'
SCAN_INTERVAL = timedelta(seconds=30) SCAN_INTERVAL = timedelta(seconds=30)
ATTR_CHANGED_BY = 'changed_by' ATTR_CHANGED_BY = 'changed_by'
FORMAT_TEXT = 'text'
FORMAT_NUMBER = 'number'
ENTITY_ID_FORMAT = DOMAIN + '.{}' ENTITY_ID_FORMAT = DOMAIN + '.{}'

View File

@ -99,7 +99,7 @@ class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel):
@property @property
def code_format(self): def code_format(self):
"""Return one or more digits/characters.""" """Return one or more digits/characters."""
return 'Number' return alarm.FORMAT_NUMBER
@property @property
def state(self): def state(self):

View File

@ -81,8 +81,8 @@ class AlarmDotCom(alarm.AlarmControlPanel):
if self._code is None: if self._code is None:
return None return None
if isinstance(self._code, str) and re.search('^\\d+$', self._code): if isinstance(self._code, str) and re.search('^\\d+$', self._code):
return 'Number' return alarm.FORMAT_NUMBER
return 'Any' return alarm.FORMAT_TEXT
@property @property
def state(self): def state(self):

View File

@ -17,7 +17,7 @@ from homeassistant.components.arlo import (
DATA_ARLO, CONF_ATTRIBUTION, SIGNAL_UPDATE_ARLO) DATA_ARLO, CONF_ATTRIBUTION, SIGNAL_UPDATE_ARLO)
from homeassistant.const import ( from homeassistant.const import (
ATTR_ATTRIBUTION, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, ATTR_ATTRIBUTION, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
STATE_ALARM_DISARMED) STATE_ALARM_DISARMED, STATE_ALARM_ARMED_NIGHT)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -25,6 +25,7 @@ ARMED = 'armed'
CONF_HOME_MODE_NAME = 'home_mode_name' CONF_HOME_MODE_NAME = 'home_mode_name'
CONF_AWAY_MODE_NAME = 'away_mode_name' CONF_AWAY_MODE_NAME = 'away_mode_name'
CONF_NIGHT_MODE_NAME = 'night_mode_name'
DEPENDENCIES = ['arlo'] DEPENDENCIES = ['arlo']
@ -35,6 +36,7 @@ ICON = 'mdi:security'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_HOME_MODE_NAME, default=ARMED): cv.string, vol.Optional(CONF_HOME_MODE_NAME, default=ARMED): cv.string,
vol.Optional(CONF_AWAY_MODE_NAME, default=ARMED): cv.string, vol.Optional(CONF_AWAY_MODE_NAME, default=ARMED): cv.string,
vol.Optional(CONF_NIGHT_MODE_NAME, default=ARMED): cv.string,
}) })
@ -47,21 +49,23 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
home_mode_name = config.get(CONF_HOME_MODE_NAME) home_mode_name = config.get(CONF_HOME_MODE_NAME)
away_mode_name = config.get(CONF_AWAY_MODE_NAME) away_mode_name = config.get(CONF_AWAY_MODE_NAME)
night_mode_name = config.get(CONF_NIGHT_MODE_NAME)
base_stations = [] base_stations = []
for base_station in arlo.base_stations: for base_station in arlo.base_stations:
base_stations.append(ArloBaseStation(base_station, home_mode_name, base_stations.append(ArloBaseStation(base_station, home_mode_name,
away_mode_name)) away_mode_name, night_mode_name))
add_entities(base_stations, True) add_entities(base_stations, True)
class ArloBaseStation(AlarmControlPanel): class ArloBaseStation(AlarmControlPanel):
"""Representation of an Arlo Alarm Control Panel.""" """Representation of an Arlo Alarm Control Panel."""
def __init__(self, data, home_mode_name, away_mode_name): def __init__(self, data, home_mode_name, away_mode_name, night_mode_name):
"""Initialize the alarm control panel.""" """Initialize the alarm control panel."""
self._base_station = data self._base_station = data
self._home_mode_name = home_mode_name self._home_mode_name = home_mode_name
self._away_mode_name = away_mode_name self._away_mode_name = away_mode_name
self._night_mode_name = night_mode_name
self._state = None self._state = None
@property @property
@ -105,6 +109,10 @@ class ArloBaseStation(AlarmControlPanel):
"""Send arm home command. Uses custom mode.""" """Send arm home command. Uses custom mode."""
self._base_station.mode = self._home_mode_name self._base_station.mode = self._home_mode_name
async def async_alarm_arm_night(self, code=None):
"""Send arm night command. Uses custom mode."""
self._base_station.mode = self._night_mode_name
@property @property
def name(self): def name(self):
"""Return the name of the base station.""" """Return the name of the base station."""
@ -128,4 +136,6 @@ class ArloBaseStation(AlarmControlPanel):
return STATE_ALARM_ARMED_HOME return STATE_ALARM_ARMED_HOME
if mode == self._away_mode_name: if mode == self._away_mode_name:
return STATE_ALARM_ARMED_AWAY return STATE_ALARM_ARMED_AWAY
if mode == self._night_mode_name:
return STATE_ALARM_ARMED_NIGHT
return mode return mode

View File

@ -80,7 +80,7 @@ class Concord232Alarm(alarm.AlarmControlPanel):
@property @property
def code_format(self): def code_format(self):
"""Return the characters if code is defined.""" """Return the characters if code is defined."""
return 'Number' return alarm.FORMAT_NUMBER
@property @property
def state(self): def state(self):

View File

@ -116,7 +116,7 @@ class ElkArea(ElkEntity, alarm.AlarmControlPanel):
@property @property
def code_format(self): def code_format(self):
"""Return the alarm code format.""" """Return the alarm code format."""
return '^[0-9]{4}([0-9]{2})?$' return alarm.FORMAT_NUMBER
@property @property
def state(self): def state(self):

View File

@ -104,7 +104,7 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel):
"""Regex for code format or None if no code is required.""" """Regex for code format or None if no code is required."""
if self._code: if self._code:
return None return None
return 'Number' return alarm.FORMAT_NUMBER
@property @property
def state(self): def state(self):

View File

@ -0,0 +1,117 @@
"""
Support for Homekit Alarm Control Panel.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/alarm_control_panel.homekit_controller/
"""
import logging
from homeassistant.components.homekit_controller import (HomeKitEntity,
KNOWN_ACCESSORIES)
from homeassistant.components.alarm_control_panel import AlarmControlPanel
from homeassistant.const import (
STATE_ALARM_DISARMED, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_NIGHT, STATE_ALARM_TRIGGERED)
from homeassistant.const import ATTR_BATTERY_LEVEL
DEPENDENCIES = ['homekit_controller']
ICON = 'mdi:security'
_LOGGER = logging.getLogger(__name__)
CURRENT_STATE_MAP = {
0: STATE_ALARM_ARMED_HOME,
1: STATE_ALARM_ARMED_AWAY,
2: STATE_ALARM_ARMED_NIGHT,
3: STATE_ALARM_DISARMED,
4: STATE_ALARM_TRIGGERED
}
TARGET_STATE_MAP = {
STATE_ALARM_ARMED_HOME: 0,
STATE_ALARM_ARMED_AWAY: 1,
STATE_ALARM_ARMED_NIGHT: 2,
STATE_ALARM_DISARMED: 3,
}
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up Homekit Alarm Control Panel support."""
if discovery_info is None:
return
accessory = hass.data[KNOWN_ACCESSORIES][discovery_info['serial']]
add_entities([HomeKitAlarmControlPanel(accessory, discovery_info)],
True)
class HomeKitAlarmControlPanel(HomeKitEntity, AlarmControlPanel):
"""Representation of a Homekit Alarm Control Panel."""
def __init__(self, *args):
"""Initialise the Alarm Control Panel."""
super().__init__(*args)
self._state = None
self._battery_level = None
def update_characteristics(self, characteristics):
"""Synchronise the Alarm Control Panel state with Home Assistant."""
# pylint: disable=import-error
from homekit.model.characteristics import CharacteristicsTypes
for characteristic in characteristics:
ctype = characteristic['type']
ctype = CharacteristicsTypes.get_short(ctype)
if ctype == "security-system-state.current":
self._chars['security-system-state.current'] = \
characteristic['iid']
self._state = CURRENT_STATE_MAP[characteristic['value']]
elif ctype == "security-system-state.target":
self._chars['security-system-state.target'] = \
characteristic['iid']
elif ctype == "battery-level":
self._chars['battery-level'] = characteristic['iid']
self._battery_level = characteristic['value']
@property
def icon(self):
"""Return icon."""
return ICON
@property
def state(self):
"""Return the state of the device."""
return self._state
def alarm_disarm(self, code=None):
"""Send disarm command."""
self.set_alarm_state(STATE_ALARM_DISARMED, code)
def alarm_arm_away(self, code=None):
"""Send arm command."""
self.set_alarm_state(STATE_ALARM_ARMED_AWAY, code)
def alarm_arm_home(self, code=None):
"""Send stay command."""
self.set_alarm_state(STATE_ALARM_ARMED_HOME, code)
def alarm_arm_night(self, code=None):
"""Send night command."""
self.set_alarm_state(STATE_ALARM_ARMED_NIGHT, code)
def set_alarm_state(self, state, code=None):
"""Send state command."""
characteristics = [{'aid': self._aid,
'iid': self._chars['security-system-state.target'],
'value': TARGET_STATE_MAP[state]}]
self.put_characteristics(characteristics)
@property
def device_state_attributes(self):
"""Return the optional state attributes."""
if self._battery_level is None:
return None
return {
ATTR_BATTERY_LEVEL: self._battery_level,
}

View File

@ -82,8 +82,8 @@ class IAlarmPanel(alarm.AlarmControlPanel):
if self._code is None: if self._code is None:
return None return None
if isinstance(self._code, str) and re.search('^\\d+$', self._code): if isinstance(self._code, str) and re.search('^\\d+$', self._code):
return 'Number' return alarm.FORMAT_NUMBER
return 'Any' return alarm.FORMAT_TEXT
@property @property
def state(self): def state(self):

View File

@ -129,8 +129,8 @@ class IFTTTAlarmPanel(alarm.AlarmControlPanel):
if self._code is None: if self._code is None:
return None return None
if isinstance(self._code, str) and re.search('^\\d+$', self._code): if isinstance(self._code, str) and re.search('^\\d+$', self._code):
return 'Number' return alarm.FORMAT_NUMBER
return 'Any' return alarm.FORMAT_TEXT
def alarm_disarm(self, code=None): def alarm_disarm(self, code=None):
"""Send disarm command.""" """Send disarm command."""

View File

@ -207,8 +207,8 @@ class ManualAlarm(alarm.AlarmControlPanel, RestoreEntity):
if self._code is None: if self._code is None:
return None return None
if isinstance(self._code, str) and re.search('^\\d+$', self._code): if isinstance(self._code, str) and re.search('^\\d+$', self._code):
return 'Number' return alarm.FORMAT_NUMBER
return 'Any' return alarm.FORMAT_TEXT
def alarm_disarm(self, code=None): def alarm_disarm(self, code=None):
"""Send disarm command.""" """Send disarm command."""

View File

@ -241,8 +241,8 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel):
if self._code is None: if self._code is None:
return None return None
if isinstance(self._code, str) and re.search('^\\d+$', self._code): if isinstance(self._code, str) and re.search('^\\d+$', self._code):
return 'Number' return alarm.FORMAT_NUMBER
return 'Any' return alarm.FORMAT_TEXT
def alarm_disarm(self, code=None): def alarm_disarm(self, code=None):
"""Send disarm command.""" """Send disarm command."""

View File

@ -59,7 +59,7 @@ class NessAlarmPanel(alarm.AlarmControlPanel):
@property @property
def code_format(self): def code_format(self):
"""Return the regex for code format or None if no code is required.""" """Return the regex for code format or None if no code is required."""
return 'Number' return alarm.FORMAT_NUMBER
@property @property
def state(self): def state(self):

View File

@ -70,7 +70,7 @@ class NX584Alarm(alarm.AlarmControlPanel):
@property @property
def code_format(self): def code_format(self):
"""Return one or more digits/characters.""" """Return one or more digits/characters."""
return 'Number' return alarm.FORMAT_NUMBER
@property @property
def state(self): def state(self):

View File

@ -64,7 +64,7 @@ class SatelIntegraAlarmPanel(alarm.AlarmControlPanel):
@property @property
def code_format(self): def code_format(self):
"""Return the regex for code format or None if no code is required.""" """Return the regex for code format or None if no code is required."""
return 'Number' return alarm.FORMAT_NUMBER
@property @property
def state(self): def state(self):

View File

@ -61,7 +61,7 @@ class VerisureAlarm(alarm.AlarmControlPanel):
@property @property
def code_format(self): def code_format(self):
"""Return one or more digits/characters.""" """Return one or more digits/characters."""
return 'Number' return alarm.FORMAT_NUMBER
@property @property
def changed_by(self): def changed_by(self):

View File

@ -46,9 +46,7 @@ ALERT_SCHEMA = vol.Schema({
vol.Required(CONF_NOTIFIERS): cv.ensure_list}) vol.Required(CONF_NOTIFIERS): cv.ensure_list})
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({ DOMAIN: cv.schema_with_slug_keys(ALERT_SCHEMA),
cv.slug: ALERT_SCHEMA,
}),
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)

View File

@ -72,6 +72,6 @@ async def async_setup(hass, config):
pass pass
else: else:
smart_home_config = smart_home_config or SMART_HOME_SCHEMA({}) smart_home_config = smart_home_config or SMART_HOME_SCHEMA({})
smart_home.async_setup(hass, smart_home_config) await smart_home.async_setup(hass, smart_home_config)
return True return True

View File

@ -27,8 +27,9 @@ from homeassistant.const import (
CONF_NAME, SERVICE_LOCK, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE, CONF_NAME, SERVICE_LOCK, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE,
SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP,
SERVICE_SET_COVER_POSITION, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_SET_COVER_POSITION, SERVICE_TURN_OFF, SERVICE_TURN_ON,
SERVICE_UNLOCK, SERVICE_VOLUME_SET, STATE_LOCKED, STATE_ON, STATE_UNLOCKED, SERVICE_UNLOCK, SERVICE_VOLUME_SET, STATE_LOCKED, STATE_ON,
TEMP_CELSIUS, TEMP_FAHRENHEIT, MATCH_ALL) STATE_UNAVAILABLE, STATE_UNLOCKED, TEMP_CELSIUS, TEMP_FAHRENHEIT,
MATCH_ALL)
import homeassistant.core as ha import homeassistant.core as ha
import homeassistant.util.color as color_util import homeassistant.util.color as color_util
from homeassistant.util.decorator import Registry from homeassistant.util.decorator import Registry
@ -393,6 +394,37 @@ class _AlexaInterface:
} }
class _AlexaEndpointHealth(_AlexaInterface):
"""Implements Alexa.EndpointHealth.
https://developer.amazon.com/docs/smarthome/state-reporting-for-a-smart-home-skill.html#report-state-when-alexa-requests-it
"""
def __init__(self, hass, entity):
super().__init__(entity)
self.hass = hass
def name(self):
return 'Alexa.EndpointHealth'
def properties_supported(self):
return [{'name': 'connectivity'}]
def properties_proactively_reported(self):
return False
def properties_retrievable(self):
return True
def get_property(self, name):
if name != 'connectivity':
raise _UnsupportedProperty(name)
if self.entity.state == STATE_UNAVAILABLE:
return {'value': 'UNREACHABLE'}
return {'value': 'OK'}
class _AlexaPowerController(_AlexaInterface): class _AlexaPowerController(_AlexaInterface):
"""Implements Alexa.PowerController. """Implements Alexa.PowerController.
@ -769,7 +801,8 @@ class _GenericCapabilities(_AlexaEntity):
return [_DisplayCategory.OTHER] return [_DisplayCategory.OTHER]
def interfaces(self): def interfaces(self):
return [_AlexaPowerController(self.entity)] return [_AlexaPowerController(self.entity),
_AlexaEndpointHealth(self.hass, self.entity)]
@ENTITY_ADAPTERS.register(switch.DOMAIN) @ENTITY_ADAPTERS.register(switch.DOMAIN)
@ -778,7 +811,8 @@ class _SwitchCapabilities(_AlexaEntity):
return [_DisplayCategory.SWITCH] return [_DisplayCategory.SWITCH]
def interfaces(self): def interfaces(self):
return [_AlexaPowerController(self.entity)] return [_AlexaPowerController(self.entity),
_AlexaEndpointHealth(self.hass, self.entity)]
@ENTITY_ADAPTERS.register(climate.DOMAIN) @ENTITY_ADAPTERS.register(climate.DOMAIN)
@ -792,6 +826,7 @@ class _ClimateCapabilities(_AlexaEntity):
yield _AlexaPowerController(self.entity) yield _AlexaPowerController(self.entity)
yield _AlexaThermostatController(self.hass, self.entity) yield _AlexaThermostatController(self.hass, self.entity)
yield _AlexaTemperatureSensor(self.hass, self.entity) yield _AlexaTemperatureSensor(self.hass, self.entity)
yield _AlexaEndpointHealth(self.hass, self.entity)
@ENTITY_ADAPTERS.register(cover.DOMAIN) @ENTITY_ADAPTERS.register(cover.DOMAIN)
@ -804,6 +839,7 @@ class _CoverCapabilities(_AlexaEntity):
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if supported & cover.SUPPORT_SET_POSITION: if supported & cover.SUPPORT_SET_POSITION:
yield _AlexaPercentageController(self.entity) yield _AlexaPercentageController(self.entity)
yield _AlexaEndpointHealth(self.hass, self.entity)
@ENTITY_ADAPTERS.register(light.DOMAIN) @ENTITY_ADAPTERS.register(light.DOMAIN)
@ -821,6 +857,7 @@ class _LightCapabilities(_AlexaEntity):
yield _AlexaColorController(self.entity) yield _AlexaColorController(self.entity)
if supported & light.SUPPORT_COLOR_TEMP: if supported & light.SUPPORT_COLOR_TEMP:
yield _AlexaColorTemperatureController(self.entity) yield _AlexaColorTemperatureController(self.entity)
yield _AlexaEndpointHealth(self.hass, self.entity)
@ENTITY_ADAPTERS.register(fan.DOMAIN) @ENTITY_ADAPTERS.register(fan.DOMAIN)
@ -833,6 +870,7 @@ class _FanCapabilities(_AlexaEntity):
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if supported & fan.SUPPORT_SET_SPEED: if supported & fan.SUPPORT_SET_SPEED:
yield _AlexaPercentageController(self.entity) yield _AlexaPercentageController(self.entity)
yield _AlexaEndpointHealth(self.hass, self.entity)
@ENTITY_ADAPTERS.register(lock.DOMAIN) @ENTITY_ADAPTERS.register(lock.DOMAIN)
@ -841,7 +879,8 @@ class _LockCapabilities(_AlexaEntity):
return [_DisplayCategory.SMARTLOCK] return [_DisplayCategory.SMARTLOCK]
def interfaces(self): def interfaces(self):
return [_AlexaLockController(self.entity)] return [_AlexaLockController(self.entity),
_AlexaEndpointHealth(self.hass, self.entity)]
@ENTITY_ADAPTERS.register(media_player.DOMAIN) @ENTITY_ADAPTERS.register(media_player.DOMAIN)
@ -851,6 +890,7 @@ class _MediaPlayerCapabilities(_AlexaEntity):
def interfaces(self): def interfaces(self):
yield _AlexaPowerController(self.entity) yield _AlexaPowerController(self.entity)
yield _AlexaEndpointHealth(self.hass, self.entity)
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if supported & media_player.SUPPORT_VOLUME_SET: if supported & media_player.SUPPORT_VOLUME_SET:
@ -913,6 +953,7 @@ class _SensorCapabilities(_AlexaEntity):
TEMP_CELSIUS, TEMP_CELSIUS,
): ):
yield _AlexaTemperatureSensor(self.hass, self.entity) yield _AlexaTemperatureSensor(self.hass, self.entity)
yield _AlexaEndpointHealth(self.hass, self.entity)
@ENTITY_ADAPTERS.register(binary_sensor.DOMAIN) @ENTITY_ADAPTERS.register(binary_sensor.DOMAIN)
@ -934,6 +975,8 @@ class _BinarySensorCapabilities(_AlexaEntity):
elif sensor_type is self.TYPE_MOTION: elif sensor_type is self.TYPE_MOTION:
yield _AlexaMotionSensor(self.hass, self.entity) yield _AlexaMotionSensor(self.hass, self.entity)
yield _AlexaEndpointHealth(self.hass, self.entity)
def get_type(self): def get_type(self):
"""Return the type of binary sensor.""" """Return the type of binary sensor."""
attrs = self.entity.attributes attrs = self.entity.attributes
@ -993,8 +1036,7 @@ class Config:
self.entity_config = entity_config or {} self.entity_config = entity_config or {}
@ha.callback async def async_setup(hass, config):
def async_setup(hass, config):
"""Activate Smart Home functionality of Alexa component. """Activate Smart Home functionality of Alexa component.
This is optional, triggered by having a `smart_home:` sub-section in the This is optional, triggered by having a `smart_home:` sub-section in the
@ -1020,8 +1062,7 @@ def async_setup(hass, config):
hass.http.register_view(SmartHomeView(smart_home_config)) hass.http.register_view(SmartHomeView(smart_home_config))
if AUTH_KEY in hass.data: if AUTH_KEY in hass.data:
hass.loop.create_task( await async_enable_proactive_mode(hass, smart_home_config)
async_enable_proactive_mode(hass, smart_home_config))
async def async_enable_proactive_mode(hass, smart_home_config): async def async_enable_proactive_mode(hass, smart_home_config):
@ -1337,8 +1378,7 @@ async def async_send_changereport_message(hass, config, alexa_entity):
return return
headers = { headers = {
"Authorization": "Bearer {}".format(token), "Authorization": "Bearer {}".format(token)
"Content-Type": "application/json;charset=UTF-8"
} }
endpoint = alexa_entity.entity_id() endpoint = alexa_entity.entity_id()
@ -1359,14 +1399,14 @@ async def async_send_changereport_message(hass, config, alexa_entity):
payload=payload) payload=payload)
message.set_endpoint_full(token, endpoint) message.set_endpoint_full(token, endpoint)
message_str = json.dumps(message.serialize()) message_serialized = message.serialize()
try: try:
session = aiohttp_client.async_get_clientsession(hass) session = aiohttp_client.async_get_clientsession(hass)
with async_timeout.timeout(DEFAULT_TIMEOUT, loop=hass.loop): with async_timeout.timeout(DEFAULT_TIMEOUT, loop=hass.loop):
response = await session.post(config.endpoint, response = await session.post(config.endpoint,
headers=headers, headers=headers,
data=message_str, json=message_serialized,
allow_redirects=True) allow_redirects=True)
except (asyncio.TimeoutError, aiohttp.ClientError): except (asyncio.TimeoutError, aiohttp.ClientError):
@ -1375,7 +1415,7 @@ async def async_send_changereport_message(hass, config, alexa_entity):
response_text = await response.text() response_text = await response.text()
_LOGGER.debug("Sent: %s", message_str) _LOGGER.debug("Sent: %s", json.dumps(message_serialized))
_LOGGER.debug("Received (%s): %s", response.status, response_text) _LOGGER.debug("Received (%s): %s", response.status, response_text)
if response.status != 202: if response.status != 202:

View File

@ -26,6 +26,8 @@ CONF_SSH_KEY = 'ssh_key'
CONF_REQUIRE_IP = 'require_ip' CONF_REQUIRE_IP = 'require_ip'
DEFAULT_SSH_PORT = 22 DEFAULT_SSH_PORT = 22
SECRET_GROUP = 'Password or SSH Key' SECRET_GROUP = 'Password or SSH Key'
CONF_SENSORS = 'sensors'
SENSOR_TYPES = ['upload_speed', 'download_speed', 'download', 'upload']
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({ DOMAIN: vol.Schema({
@ -37,7 +39,9 @@ CONFIG_SCHEMA = vol.Schema({
vol.Optional(CONF_REQUIRE_IP, default=True): cv.boolean, vol.Optional(CONF_REQUIRE_IP, default=True): cv.boolean,
vol.Exclusive(CONF_PASSWORD, SECRET_GROUP): cv.string, vol.Exclusive(CONF_PASSWORD, SECRET_GROUP): cv.string,
vol.Exclusive(CONF_SSH_KEY, SECRET_GROUP): cv.isfile, vol.Exclusive(CONF_SSH_KEY, SECRET_GROUP): cv.isfile,
vol.Exclusive(CONF_PUB_KEY, SECRET_GROUP): cv.isfile vol.Exclusive(CONF_PUB_KEY, SECRET_GROUP): cv.isfile,
vol.Optional(CONF_SENSORS): vol.All(
cv.ensure_list, [vol.In(SENSOR_TYPES)]),
}), }),
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
@ -62,7 +66,8 @@ async def async_setup(hass, config):
hass.data[DATA_ASUSWRT] = api hass.data[DATA_ASUSWRT] = api
hass.async_create_task(async_load_platform( hass.async_create_task(async_load_platform(
hass, 'sensor', DOMAIN, {}, config)) hass, 'sensor', DOMAIN, config[DOMAIN].get(CONF_SENSORS), config))
hass.async_create_task(async_load_platform( hass.async_create_task(async_load_platform(
hass, 'device_tracker', DOMAIN, {}, config)) hass, 'device_tracker', DOMAIN, {}, config))
return True return True

View File

@ -9,11 +9,11 @@
}, },
"step": { "step": {
"init": { "init": {
"description": "Bitte w\u00e4hle einen der Benachrichtigungsdienste:", "description": "Bitte w\u00e4hlen Sie einen der Benachrichtigungsdienste:",
"title": "Einmal Passwort f\u00fcr Notify einrichten" "title": "Einmal Passwort f\u00fcr Notify einrichten"
}, },
"setup": { "setup": {
"description": "Ein Einmal-Passwort wurde per ** notify gesendet. {notify_service} **. Bitte gebe es unten ein:", "description": "Ein Einmal-Passwort wurde per **notify.{notify_service}** gesendet. Bitte geben Sie es unten ein:",
"title": "\u00dcberpr\u00fcfe das Setup" "title": "\u00dcberpr\u00fcfe das Setup"
} }
}, },

View File

@ -0,0 +1,7 @@
{
"mfa_setup": {
"totp": {
"title": ""
}
}
}

View File

@ -375,8 +375,6 @@ def _async_get_action(hass, config, name):
async def action(entity_id, variables, context): async def action(entity_id, variables, context):
"""Execute an action.""" """Execute an action."""
_LOGGER.info('Executing %s', name) _LOGGER.info('Executing %s', name)
hass.components.logbook.async_log_entry(
name, 'has been triggered', DOMAIN, entity_id)
try: try:
await script_obj.async_run(variables, context) await script_obj.async_run(variables, context)

View File

@ -1,9 +1,9 @@
""" """
Offer geo location automation rules. Offer geolocation automation rules.
For more details about this automation trigger, please refer to the For more details about this automation trigger, please refer to the
documentation at documentation at
https://home-assistant.io/docs/automation/trigger/#geo-location-trigger https://home-assistant.io/docs/automation/trigger/#geolocation-trigger
""" """
import voluptuous as vol import voluptuous as vol

View File

@ -13,30 +13,18 @@ from homeassistant.const import CONF_AT, CONF_PLATFORM
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.event import async_track_time_change from homeassistant.helpers.event import async_track_time_change
CONF_HOURS = 'hours'
CONF_MINUTES = 'minutes'
CONF_SECONDS = 'seconds'
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
TRIGGER_SCHEMA = vol.All(vol.Schema({ TRIGGER_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): 'time', vol.Required(CONF_PLATFORM): 'time',
CONF_AT: cv.time, vol.Required(CONF_AT): cv.time,
CONF_HOURS: vol.Any(vol.Coerce(int), vol.Coerce(str)), })
CONF_MINUTES: vol.Any(vol.Coerce(int), vol.Coerce(str)),
CONF_SECONDS: vol.Any(vol.Coerce(int), vol.Coerce(str)),
}), cv.has_at_least_one_key(CONF_HOURS, CONF_MINUTES, CONF_SECONDS, CONF_AT))
async def async_trigger(hass, config, action, automation_info): async def async_trigger(hass, config, action, automation_info):
"""Listen for state changes based on configuration.""" """Listen for state changes based on configuration."""
if CONF_AT in config: at_time = config.get(CONF_AT)
at_time = config.get(CONF_AT) hours, minutes, seconds = at_time.hour, at_time.minute, at_time.second
hours, minutes, seconds = at_time.hour, at_time.minute, at_time.second
else:
hours = config.get(CONF_HOURS)
minutes = config.get(CONF_MINUTES)
seconds = config.get(CONF_SECONDS)
@callback @callback
def time_automation_listener(now): def time_automation_listener(now):

View File

@ -0,0 +1,53 @@
"""
Offer time listening automation rules.
For more details about this automation rule, please refer to the documentation
at https://home-assistant.io/docs/automation/trigger/#time-trigger
"""
import logging
import voluptuous as vol
from homeassistant.core import callback
from homeassistant.const import CONF_PLATFORM
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.event import async_track_time_change
CONF_HOURS = 'hours'
CONF_MINUTES = 'minutes'
CONF_SECONDS = 'seconds'
_LOGGER = logging.getLogger(__name__)
TRIGGER_SCHEMA = vol.All(vol.Schema({
vol.Required(CONF_PLATFORM): 'time_pattern',
CONF_HOURS: vol.Any(vol.Coerce(int), vol.Coerce(str)),
CONF_MINUTES: vol.Any(vol.Coerce(int), vol.Coerce(str)),
CONF_SECONDS: vol.Any(vol.Coerce(int), vol.Coerce(str)),
}), cv.has_at_least_one_key(CONF_HOURS, CONF_MINUTES, CONF_SECONDS))
async def async_trigger(hass, config, action, automation_info):
"""Listen for state changes based on configuration."""
hours = config.get(CONF_HOURS)
minutes = config.get(CONF_MINUTES)
seconds = config.get(CONF_SECONDS)
# If larger units are specified, default the smaller units to zero
if minutes is None and hours is not None:
minutes = 0
if seconds is None and minutes is not None:
seconds = 0
@callback
def time_automation_listener(now):
"""Listen for time changes and calls action."""
hass.async_run_job(action, {
'trigger': {
'platform': 'time_pattern',
'now': now,
},
})
return async_track_time_change(hass, time_automation_listener,
hour=hours, minute=minutes, second=seconds)

View File

@ -50,9 +50,7 @@ DEVICE_SCHEMA = vol.Schema({
}) })
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({ DOMAIN: cv.schema_with_slug_keys(DEVICE_SCHEMA),
cv.slug: DEVICE_SCHEMA,
}),
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
SERVICE_VAPIX_CALL = 'vapix_call' SERVICE_VAPIX_CALL = 'vapix_call'

View File

@ -1,147 +0,0 @@
"""
Support for deCONZ binary sensor.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.deconz/
"""
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.deconz.const import (
ATTR_DARK, ATTR_ON, CONF_ALLOW_CLIP_SENSOR, DECONZ_REACHABLE,
DOMAIN as DECONZ_DOMAIN)
from homeassistant.const import ATTR_BATTERY_LEVEL
from homeassistant.core import callback
from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE
from homeassistant.helpers.dispatcher import async_dispatcher_connect
DEPENDENCIES = ['deconz']
async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Old way of setting up deCONZ binary sensors."""
pass
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the deCONZ binary sensor."""
gateway = hass.data[DECONZ_DOMAIN]
@callback
def async_add_sensor(sensors):
"""Add binary sensor from deCONZ."""
from pydeconz.sensor import DECONZ_BINARY_SENSOR
entities = []
allow_clip_sensor = config_entry.data.get(CONF_ALLOW_CLIP_SENSOR, True)
for sensor in sensors:
if sensor.type in DECONZ_BINARY_SENSOR and \
not (not allow_clip_sensor and sensor.type.startswith('CLIP')):
entities.append(DeconzBinarySensor(sensor, gateway))
async_add_entities(entities, True)
gateway.listeners.append(
async_dispatcher_connect(hass, 'deconz_new_sensor', async_add_sensor))
async_add_sensor(gateway.api.sensors.values())
class DeconzBinarySensor(BinarySensorDevice):
"""Representation of a binary sensor."""
def __init__(self, sensor, gateway):
"""Set up sensor and add update callback to get data from websocket."""
self._sensor = sensor
self.gateway = gateway
self.unsub_dispatcher = None
async def async_added_to_hass(self):
"""Subscribe sensors events."""
self._sensor.register_async_callback(self.async_update_callback)
self.gateway.deconz_ids[self.entity_id] = self._sensor.deconz_id
self.unsub_dispatcher = async_dispatcher_connect(
self.hass, DECONZ_REACHABLE, self.async_update_callback)
async def async_will_remove_from_hass(self) -> None:
"""Disconnect sensor object when removed."""
if self.unsub_dispatcher is not None:
self.unsub_dispatcher()
self._sensor.remove_callback(self.async_update_callback)
self._sensor = None
@callback
def async_update_callback(self, reason):
"""Update the sensor's state.
If reason is that state is updated,
or reachable has changed or battery has changed.
"""
if reason['state'] or \
'reachable' in reason['attr'] or \
'battery' in reason['attr'] or \
'on' in reason['attr']:
self.async_schedule_update_ha_state()
@property
def is_on(self):
"""Return true if sensor is on."""
return self._sensor.is_tripped
@property
def name(self):
"""Return the name of the sensor."""
return self._sensor.name
@property
def unique_id(self):
"""Return a unique identifier for this sensor."""
return self._sensor.uniqueid
@property
def device_class(self):
"""Return the class of the sensor."""
return self._sensor.sensor_class
@property
def icon(self):
"""Return the icon to use in the frontend."""
return self._sensor.sensor_icon
@property
def available(self):
"""Return True if sensor is available."""
return self.gateway.available and self._sensor.reachable
@property
def should_poll(self):
"""No polling needed."""
return False
@property
def device_state_attributes(self):
"""Return the state attributes of the sensor."""
from pydeconz.sensor import PRESENCE
attr = {}
if self._sensor.battery:
attr[ATTR_BATTERY_LEVEL] = self._sensor.battery
if self._sensor.on is not None:
attr[ATTR_ON] = self._sensor.on
if self._sensor.type in PRESENCE and self._sensor.dark is not None:
attr[ATTR_DARK] = self._sensor.dark
return attr
@property
def device_info(self):
"""Return a device description for device registry."""
if (self._sensor.uniqueid is None or
self._sensor.uniqueid.count(':') != 7):
return None
serial = self._sensor.uniqueid.split('-', 1)[0]
bridgeid = self.gateway.api.config.bridgeid
return {
'connections': {(CONNECTION_ZIGBEE, serial)},
'identifiers': {(DECONZ_DOMAIN, serial)},
'manufacturer': self._sensor.manufacturer,
'model': self._sensor.modelid,
'name': self._sensor.name,
'sw_version': self._sensor.swversion,
'via_hub': (DECONZ_DOMAIN, bridgeid),
}

View File

@ -9,7 +9,7 @@ import logging
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
BinarySensorDevice, ENTITY_ID_FORMAT) BinarySensorDevice, ENTITY_ID_FORMAT)
from homeassistant.components.fibaro import ( from homeassistant.components.fibaro import (
FIBARO_CONTROLLER, FIBARO_DEVICES, FibaroDevice) FIBARO_DEVICES, FibaroDevice)
from homeassistant.const import (CONF_DEVICE_CLASS, CONF_ICON) from homeassistant.const import (CONF_DEVICE_CLASS, CONF_ICON)
DEPENDENCIES = ['fibaro'] DEPENDENCIES = ['fibaro']
@ -33,17 +33,17 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
return return
add_entities( add_entities(
[FibaroBinarySensor(device, hass.data[FIBARO_CONTROLLER]) [FibaroBinarySensor(device)
for device in hass.data[FIBARO_DEVICES]['binary_sensor']], True) for device in hass.data[FIBARO_DEVICES]['binary_sensor']], True)
class FibaroBinarySensor(FibaroDevice, BinarySensorDevice): class FibaroBinarySensor(FibaroDevice, BinarySensorDevice):
"""Representation of a Fibaro Binary Sensor.""" """Representation of a Fibaro Binary Sensor."""
def __init__(self, fibaro_device, controller): def __init__(self, fibaro_device):
"""Initialize the binary_sensor.""" """Initialize the binary_sensor."""
self._state = None self._state = None
super().__init__(fibaro_device, controller) super().__init__(fibaro_device)
self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id) self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id)
stype = None stype = None
devconf = fibaro_device.device_config devconf = fibaro_device.device_config

View File

@ -5,7 +5,7 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.hive/ https://home-assistant.io/components/binary_sensor.hive/
""" """
from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.hive import DATA_HIVE from homeassistant.components.hive import DATA_HIVE, DOMAIN
DEPENDENCIES = ['hive'] DEPENDENCIES = ['hive']
@ -35,9 +35,24 @@ class HiveBinarySensorEntity(BinarySensorDevice):
self.attributes = {} self.attributes = {}
self.data_updatesource = '{}.{}'.format(self.device_type, self.data_updatesource = '{}.{}'.format(self.device_type,
self.node_id) self.node_id)
self._unique_id = '{}-{}'.format(self.node_id, self.device_type)
self.session.entities.append(self) self.session.entities.append(self)
@property
def unique_id(self):
"""Return unique ID of entity."""
return self._unique_id
@property
def device_info(self):
"""Return device information."""
return {
'identifiers': {
(DOMAIN, self.unique_id)
},
'name': self.name
}
def handle_update(self, updatesource): def handle_update(self, updatesource):
"""Handle the new update request.""" """Handle the new update request."""
if '{}.{}'.format(self.device_type, self.node_id) not in updatesource: if '{}.{}'.format(self.device_type, self.node_id) not in updatesource:

View File

@ -41,7 +41,7 @@ SENSOR_SCHEMA = vol.Schema({
}) })
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_SENSORS): vol.Schema({cv.slug: SENSOR_SCHEMA}), vol.Required(CONF_SENSORS): cv.schema_with_slug_keys(SENSOR_SCHEMA),
}) })

View File

@ -51,7 +51,7 @@ SENSOR_SCHEMA = vol.Schema({
}) })
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_SENSORS): vol.Schema({cv.slug: SENSOR_SCHEMA}), vol.Required(CONF_SENSORS): cv.schema_with_slug_keys(SENSOR_SCHEMA),
}) })

View File

@ -14,7 +14,7 @@ from homeassistant.const import CONF_NAME, WEEKDAYS
from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.binary_sensor import BinarySensorDevice
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['holidays==0.9.8'] REQUIREMENTS = ['holidays==0.9.9']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -26,6 +26,7 @@ ALL_COUNTRIES = [
'Canada', 'CA', 'Colombia', 'CO', 'Croatia', 'HR', 'Czech', 'CZ', 'Canada', 'CA', 'Colombia', 'CO', 'Croatia', 'HR', 'Czech', 'CZ',
'Denmark', 'DK', 'England', 'EuropeanCentralBank', 'ECB', 'TAR', 'Denmark', 'DK', 'England', 'EuropeanCentralBank', 'ECB', 'TAR',
'Finland', 'FI', 'France', 'FRA', 'Germany', 'DE', 'Hungary', 'HU', 'Finland', 'FI', 'France', 'FRA', 'Germany', 'DE', 'Hungary', 'HU',
'Honduras', 'HUD',
'India', 'IND', 'Ireland', 'Isle of Man', 'Italy', 'IT', 'Japan', 'JP', 'India', 'IND', 'Ireland', 'Isle of Man', 'Italy', 'IT', 'Japan', 'JP',
'Mexico', 'MX', 'Netherlands', 'NL', 'NewZealand', 'NZ', 'Mexico', 'MX', 'Netherlands', 'NL', 'NewZealand', 'NZ',
'Northern Ireland', 'Norway', 'NO', 'Polish', 'PL', 'Portugal', 'PT', 'Northern Ireland', 'Norway', 'NO', 'Polish', 'PL', 'Portugal', 'PT',

View File

@ -107,7 +107,7 @@ class XiaomiBinarySensor(XiaomiDevice, BinarySensorDevice):
def update(self): def update(self):
"""Update the sensor state.""" """Update the sensor state."""
_LOGGER.debug('Updating xiaomi sensor by polling') _LOGGER.debug('Updating xiaomi sensor (%s) by polling', self._sid)
self._get_from_hub(self._sid) self._get_from_hub(self._sid)
@ -178,7 +178,28 @@ class XiaomiMotionSensor(XiaomiBinarySensor):
self.async_schedule_update_ha_state() self.async_schedule_update_ha_state()
def parse_data(self, data, raw_data): def parse_data(self, data, raw_data):
"""Parse data sent by gateway.""" """Parse data sent by gateway.
Polling (proto v1, firmware version 1.4.1_159.0143)
>> { "cmd":"read","sid":"158..."}
<< {'model': 'motion', 'sid': '158...', 'short_id': 26331,
'cmd': 'read_ack', 'data': '{"voltage":3005}'}
Multicast messages (proto v1, firmware version 1.4.1_159.0143)
<< {'model': 'motion', 'sid': '158...', 'short_id': 26331,
'cmd': 'report', 'data': '{"status":"motion"}'}
<< {'model': 'motion', 'sid': '158...', 'short_id': 26331,
'cmd': 'report', 'data': '{"no_motion":"120"}'}
<< {'model': 'motion', 'sid': '158...', 'short_id': 26331,
'cmd': 'report', 'data': '{"no_motion":"180"}'}
<< {'model': 'motion', 'sid': '158...', 'short_id': 26331,
'cmd': 'report', 'data': '{"no_motion":"300"}'}
<< {'model': 'motion', 'sid': '158...', 'short_id': 26331,
'cmd': 'heartbeat', 'data': '{"voltage":3005}'}
"""
if raw_data['cmd'] == 'heartbeat': if raw_data['cmd'] == 'heartbeat':
_LOGGER.debug( _LOGGER.debug(
'Skipping heartbeat of the motion sensor. ' 'Skipping heartbeat of the motion sensor. '
@ -187,8 +208,7 @@ class XiaomiMotionSensor(XiaomiBinarySensor):
'11631#issuecomment-357507744).') '11631#issuecomment-357507744).')
return return
self._should_poll = False if NO_MOTION in data:
if NO_MOTION in data: # handle push from the hub
self._no_motion_since = data[NO_MOTION] self._no_motion_since = data[NO_MOTION]
self._state = False self._state = False
return True return True
@ -203,26 +223,20 @@ class XiaomiMotionSensor(XiaomiBinarySensor):
self._unsub_set_no_motion() self._unsub_set_no_motion()
self._unsub_set_no_motion = async_call_later( self._unsub_set_no_motion = async_call_later(
self._hass, self._hass,
180, 120,
self._async_set_no_motion self._async_set_no_motion
) )
else:
self._should_poll = True if self.entity_id is not None:
if self.entity_id is not None: self._hass.bus.fire('xiaomi_aqara.motion', {
self._hass.bus.fire('xiaomi_aqara.motion', { 'entity_id': self.entity_id
'entity_id': self.entity_id })
})
self._no_motion_since = 0 self._no_motion_since = 0
if self._state: if self._state:
return False return False
self._state = True self._state = True
return True return True
if value == NO_MOTION:
if not self._state:
return False
self._state = False
return True
class XiaomiDoorSensor(XiaomiBinarySensor): class XiaomiDoorSensor(XiaomiBinarySensor):

View File

@ -15,7 +15,7 @@ from homeassistant.const import (
CONF_BINARY_SENSORS, CONF_SENSORS, CONF_FILENAME, CONF_BINARY_SENSORS, CONF_SENSORS, CONF_FILENAME,
CONF_MONITORED_CONDITIONS, TEMP_FAHRENHEIT) CONF_MONITORED_CONDITIONS, TEMP_FAHRENHEIT)
REQUIREMENTS = ['blinkpy==0.11.0'] REQUIREMENTS = ['blinkpy==0.11.1']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -7,6 +7,7 @@ https://www.home-assistant.io/components/camera.proxy/
import asyncio import asyncio
import logging import logging
from datetime import timedelta
import voluptuous as vol import voluptuous as vol
from homeassistant.components.camera import PLATFORM_SCHEMA, Camera from homeassistant.components.camera import PLATFORM_SCHEMA, Camera
@ -18,7 +19,7 @@ from homeassistant.util.async_ import run_coroutine_threadsafe
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from homeassistant.components.camera import async_get_still_stream from homeassistant.components.camera import async_get_still_stream
REQUIREMENTS = ['pillow==5.3.0'] REQUIREMENTS = ['pillow==5.4.1']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -206,7 +207,7 @@ class ProxyCamera(Camera):
self._cache_images = bool( self._cache_images = bool(
config.get(CONF_IMAGE_REFRESH_RATE) config.get(CONF_IMAGE_REFRESH_RATE)
or config.get(CONF_CACHE_IMAGES)) or config.get(CONF_CACHE_IMAGES))
self._last_image_time = 0 self._last_image_time = dt_util.utc_from_timestamp(0)
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}
@ -223,7 +224,8 @@ class ProxyCamera(Camera):
now = dt_util.utcnow() now = dt_util.utcnow()
if (self._image_refresh_rate and if (self._image_refresh_rate and
now < self._last_image_time + self._image_refresh_rate): now < self._last_image_time +
timedelta(seconds=self._image_refresh_rate)):
return self._last_image return self._last_image
self._last_image_time = now self._last_image_time = now

View File

@ -107,7 +107,7 @@ class XiaomiCamera(Camera):
_LOGGER.warning("There don't appear to be any folders") _LOGGER.warning("There don't appear to be any folders")
return False return False
first_dir = dirs[-1] first_dir = latest_dir = dirs[-1]
try: try:
ftp.cwd(first_dir) ftp.cwd(first_dir)
except error_perm as exc: except error_perm as exc:

View File

@ -19,17 +19,18 @@ DEPENDENCIES = ['zoneminder']
def setup_platform(hass, config, add_entities, discovery_info=None): def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the ZoneMinder cameras.""" """Set up the ZoneMinder cameras."""
filter_urllib3_logging() filter_urllib3_logging()
zm_client = hass.data[ZONEMINDER_DOMAIN]
monitors = zm_client.get_monitors()
if not monitors:
_LOGGER.warning("Could not fetch monitors from ZoneMinder")
return
cameras = [] cameras = []
for monitor in monitors: for zm_client in hass.data[ZONEMINDER_DOMAIN].values():
_LOGGER.info("Initializing camera %s", monitor.id) monitors = zm_client.get_monitors()
cameras.append(ZoneMinderCamera(monitor, zm_client.verify_ssl)) if not monitors:
_LOGGER.warning(
"Could not fetch monitors from ZoneMinder host: %s"
)
return
for monitor in monitors:
_LOGGER.info("Initializing camera %s", monitor.id)
cameras.append(ZoneMinderCamera(monitor, zm_client.verify_ssl))
add_entities(cameras) add_entities(cameras)

View File

@ -0,0 +1,10 @@
{
"config": {
"step": {
"confirm": {
"title": ""
}
},
"title": ""
}
}

View File

@ -8,7 +8,7 @@ from homeassistant.components.climate import (
ClimateDevice, STATE_AUTO, STATE_HEAT, STATE_OFF, STATE_ON, ClimateDevice, STATE_AUTO, STATE_HEAT, STATE_OFF, STATE_ON,
SUPPORT_AUX_HEAT, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE) SUPPORT_AUX_HEAT, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE)
from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS
from homeassistant.components.hive import DATA_HIVE from homeassistant.components.hive import DATA_HIVE, DOMAIN
DEPENDENCIES = ['hive'] DEPENDENCIES = ['hive']
HIVE_TO_HASS_STATE = {'SCHEDULE': STATE_AUTO, 'MANUAL': STATE_HEAT, HIVE_TO_HASS_STATE = {'SCHEDULE': STATE_AUTO, 'MANUAL': STATE_HEAT,
@ -44,6 +44,7 @@ class HiveClimateEntity(ClimateDevice):
self.attributes = {} self.attributes = {}
self.data_updatesource = '{}.{}'.format(self.device_type, self.data_updatesource = '{}.{}'.format(self.device_type,
self.node_id) self.node_id)
self._unique_id = '{}-{}'.format(self.node_id, self.device_type)
if self.device_type == "Heating": if self.device_type == "Heating":
self.modes = [STATE_AUTO, STATE_HEAT, STATE_OFF] self.modes = [STATE_AUTO, STATE_HEAT, STATE_OFF]
@ -52,6 +53,21 @@ class HiveClimateEntity(ClimateDevice):
self.session.entities.append(self) self.session.entities.append(self)
@property
def unique_id(self):
"""Return unique ID of entity."""
return self._unique_id
@property
def device_info(self):
"""Return device information."""
return {
'identifiers': {
(DOMAIN, self.unique_id)
},
'name': self.name
}
@property @property
def supported_features(self): def supported_features(self):
"""Return the list of supported features.""" """Return the list of supported features."""

View File

@ -19,7 +19,8 @@ from homeassistant.components.climate import (
ATTR_CURRENT_HUMIDITY, ClimateDevice, DOMAIN, PLATFORM_SCHEMA, ATTR_CURRENT_HUMIDITY, ClimateDevice, DOMAIN, PLATFORM_SCHEMA,
SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE,
SUPPORT_FAN_MODE, SUPPORT_SWING_MODE, SUPPORT_FAN_MODE, SUPPORT_SWING_MODE,
SUPPORT_ON_OFF) SUPPORT_ON_OFF, STATE_HEAT, STATE_COOL, STATE_FAN_ONLY, STATE_DRY,
STATE_AUTO)
from homeassistant.exceptions import PlatformNotReady from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
@ -57,6 +58,16 @@ FIELD_TO_FLAG = {
'on': SUPPORT_ON_OFF, 'on': SUPPORT_ON_OFF,
} }
SENSIBO_TO_HA = {
"cool": STATE_COOL,
"heat": STATE_HEAT,
"fan": STATE_FAN_ONLY,
"auto": STATE_AUTO,
"dry": STATE_DRY
}
HA_TO_SENSIBO = {value: key for key, value in SENSIBO_TO_HA.items()}
async def async_setup_platform(hass, config, async_add_entities, async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None): discovery_info=None):
@ -129,9 +140,10 @@ class SensiboClimate(ClimateDevice):
self._ac_states = data['acState'] self._ac_states = data['acState']
self._status = data['connectionStatus']['isAlive'] self._status = data['connectionStatus']['isAlive']
capabilities = data['remoteCapabilities'] capabilities = data['remoteCapabilities']
self._operations = sorted(capabilities['modes'].keys()) self._operations = [SENSIBO_TO_HA[mode] for mode
self._current_capabilities = capabilities[ in capabilities['modes']]
'modes'][self.current_operation] self._current_capabilities = \
capabilities['modes'][self._ac_states['mode']]
temperature_unit_key = data.get('temperatureUnit') or \ temperature_unit_key = data.get('temperatureUnit') or \
self._ac_states.get('temperatureUnit') self._ac_states.get('temperatureUnit')
if temperature_unit_key: if temperature_unit_key:
@ -186,7 +198,7 @@ class SensiboClimate(ClimateDevice):
@property @property
def current_operation(self): def current_operation(self):
"""Return current operation ie. heat, cool, idle.""" """Return current operation ie. heat, cool, idle."""
return self._ac_states['mode'] return SENSIBO_TO_HA.get(self._ac_states['mode'])
@property @property
def current_humidity(self): def current_humidity(self):
@ -293,7 +305,8 @@ class SensiboClimate(ClimateDevice):
"""Set new target operation mode.""" """Set new target operation mode."""
with async_timeout.timeout(TIMEOUT): with async_timeout.timeout(TIMEOUT):
await self._client.async_set_ac_state_property( await self._client.async_set_ac_state_property(
self._id, 'mode', operation_mode, self._ac_states) self._id, 'mode', HA_TO_SENSIBO[operation_mode],
self._ac_states)
async def async_set_swing_mode(self, swing_mode): async def async_set_swing_mode(self, swing_mode):
"""Set new target swing operation.""" """Set new target swing operation."""

View File

@ -2,6 +2,7 @@
import asyncio import asyncio
import logging import logging
import pprint import pprint
import random
import uuid import uuid
from aiohttp import hdrs, client_exceptions, WSMsgType from aiohttp import hdrs, client_exceptions, WSMsgType
@ -107,9 +108,11 @@ class CloudIoT:
self.tries += 1 self.tries += 1
try: try:
# Sleep 2^tries seconds between retries # Sleep 2^tries + 0…tries*3 seconds between retries
self.retry_task = hass.async_create_task(asyncio.sleep( self.retry_task = hass.async_create_task(
2**min(9, self.tries), loop=hass.loop)) asyncio.sleep(2**min(9, self.tries) +
random.randint(0, self.tries * 3),
loop=hass.loop))
yield from self.retry_task yield from self.retry_task
self.retry_task = None self.retry_task = None
except asyncio.CancelledError: except asyncio.CancelledError:
@ -313,15 +316,20 @@ def async_handle_google_actions(hass, cloud, payload):
@HANDLERS.register('cloud') @HANDLERS.register('cloud')
@asyncio.coroutine async def async_handle_cloud(hass, cloud, payload):
def async_handle_cloud(hass, cloud, payload):
"""Handle an incoming IoT message for cloud component.""" """Handle an incoming IoT message for cloud component."""
action = payload['action'] action = payload['action']
if action == 'logout': if action == 'logout':
yield from cloud.logout() # Log out of Home Assistant Cloud
await cloud.logout()
_LOGGER.error("You have been logged out from Home Assistant cloud: %s", _LOGGER.error("You have been logged out from Home Assistant cloud: %s",
payload['reason']) payload['reason'])
elif action == 'refresh_auth':
# Refresh the auth token between now and payload['seconds']
hass.helpers.event.async_call_later(
random.randint(0, payload['seconds']),
lambda now: auth_api.check_token(cloud))
else: else:
_LOGGER.warning("Received unknown cloud action: %s", action) _LOGGER.warning("Received unknown cloud action: %s", action)

View File

@ -37,8 +37,8 @@ SERVICE_SCHEMA = vol.Schema({
}) })
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({ DOMAIN: cv.schema_with_slug_keys(
cv.slug: vol.Any({ vol.Any({
vol.Optional(CONF_ICON): cv.icon, vol.Optional(CONF_ICON): cv.icon,
vol.Optional(CONF_INITIAL, default=DEFAULT_INITIAL): vol.Optional(CONF_INITIAL, default=DEFAULT_INITIAL):
cv.positive_int, cv.positive_int,
@ -46,7 +46,7 @@ CONFIG_SCHEMA = vol.Schema({
vol.Optional(CONF_RESTORE, default=True): cv.boolean, vol.Optional(CONF_RESTORE, default=True): cv.boolean,
vol.Optional(CONF_STEP, default=DEFAULT_STEP): cv.positive_int, vol.Optional(CONF_STEP, default=DEFAULT_STEP): cv.positive_int,
}, None) }, None)
}) )
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)

View File

@ -27,7 +27,7 @@ COVER_SCHEMA = vol.Schema({
}) })
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_COVERS): vol.Schema({cv.slug: COVER_SCHEMA}), vol.Required(CONF_COVERS): cv.schema_with_slug_keys(COVER_SCHEMA),
}) })

View File

@ -9,7 +9,7 @@ import logging
from homeassistant.components.cover import ( from homeassistant.components.cover import (
CoverDevice, ENTITY_ID_FORMAT, ATTR_POSITION, ATTR_TILT_POSITION) CoverDevice, ENTITY_ID_FORMAT, ATTR_POSITION, ATTR_TILT_POSITION)
from homeassistant.components.fibaro import ( from homeassistant.components.fibaro import (
FIBARO_CONTROLLER, FIBARO_DEVICES, FibaroDevice) FIBARO_DEVICES, FibaroDevice)
DEPENDENCIES = ['fibaro'] DEPENDENCIES = ['fibaro']
@ -22,16 +22,16 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
return return
add_entities( add_entities(
[FibaroCover(device, hass.data[FIBARO_CONTROLLER]) for [FibaroCover(device) for
device in hass.data[FIBARO_DEVICES]['cover']], True) device in hass.data[FIBARO_DEVICES]['cover']], True)
class FibaroCover(FibaroDevice, CoverDevice): class FibaroCover(FibaroDevice, CoverDevice):
"""Representation a Fibaro Cover.""" """Representation a Fibaro Cover."""
def __init__(self, fibaro_device, controller): def __init__(self, fibaro_device):
"""Initialize the Vera device.""" """Initialize the Vera device."""
super().__init__(fibaro_device, controller) super().__init__(fibaro_device)
self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id) self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id)
@staticmethod @staticmethod

View File

@ -47,7 +47,7 @@ COVER_SCHEMA = vol.Schema({
}) })
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_COVERS): vol.Schema({cv.slug: COVER_SCHEMA}), vol.Required(CONF_COVERS): cv.schema_with_slug_keys(COVER_SCHEMA),
}) })

View File

@ -0,0 +1,305 @@
"""
Support for Homekit Cover.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/cover.homekit_controller/
"""
import logging
from homeassistant.components.homekit_controller import (HomeKitEntity,
KNOWN_ACCESSORIES)
from homeassistant.components.cover import (
CoverDevice, SUPPORT_OPEN, SUPPORT_CLOSE, SUPPORT_SET_POSITION,
SUPPORT_OPEN_TILT, SUPPORT_CLOSE_TILT, SUPPORT_SET_TILT_POSITION,
ATTR_POSITION, ATTR_TILT_POSITION)
from homeassistant.const import (
STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING)
STATE_STOPPED = 'stopped'
DEPENDENCIES = ['homekit_controller']
_LOGGER = logging.getLogger(__name__)
CURRENT_GARAGE_STATE_MAP = {
0: STATE_OPEN,
1: STATE_CLOSED,
2: STATE_OPENING,
3: STATE_CLOSING,
4: STATE_STOPPED
}
TARGET_GARAGE_STATE_MAP = {
STATE_OPEN: 0,
STATE_CLOSED: 1,
STATE_STOPPED: 2
}
CURRENT_WINDOW_STATE_MAP = {
0: STATE_OPENING,
1: STATE_CLOSING,
2: STATE_STOPPED
}
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up HomeKit Cover support."""
if discovery_info is None:
return
accessory = hass.data[KNOWN_ACCESSORIES][discovery_info['serial']]
if discovery_info['device-type'] == 'garage-door-opener':
add_entities([HomeKitGarageDoorCover(accessory, discovery_info)],
True)
else:
add_entities([HomeKitWindowCover(accessory, discovery_info)],
True)
class HomeKitGarageDoorCover(HomeKitEntity, CoverDevice):
"""Representation of a HomeKit Garage Door."""
def __init__(self, accessory, discovery_info):
"""Initialise the Cover."""
super().__init__(accessory, discovery_info)
self._name = None
self._state = None
self._obstruction_detected = None
self.lock_state = None
@property
def device_class(self):
"""Define this cover as a garage door."""
return 'garage'
def update_characteristics(self, characteristics):
"""Synchronise the Cover state with Home Assistant."""
# pylint: disable=import-error
from homekit.model.characteristics import CharacteristicsTypes
for characteristic in characteristics:
ctype = characteristic['type']
ctype = CharacteristicsTypes.get_short(ctype)
if ctype == "door-state.current":
self._chars['door-state.current'] = \
characteristic['iid']
self._state = CURRENT_GARAGE_STATE_MAP[characteristic['value']]
elif ctype == "door-state.target":
self._chars['door-state.target'] = \
characteristic['iid']
elif ctype == "obstruction-detected":
self._chars['obstruction-detected'] = characteristic['iid']
self._obstruction_detected = characteristic['value']
elif ctype == "name":
self._chars['name'] = characteristic['iid']
self._name = characteristic['value']
@property
def name(self):
"""Return the name of the cover."""
return self._name
@property
def available(self):
"""Return True if entity is available."""
return self._state is not None
@property
def supported_features(self):
"""Flag supported features."""
return SUPPORT_OPEN | SUPPORT_CLOSE
@property
def is_closed(self):
"""Return true if cover is closed, else False."""
return self._state == STATE_CLOSED
@property
def is_closing(self):
"""Return if the cover is closing or not."""
return self._state == STATE_CLOSING
@property
def is_opening(self):
"""Return if the cover is opening or not."""
return self._state == STATE_OPENING
def open_cover(self, **kwargs):
"""Send open command."""
self.set_door_state(STATE_OPEN)
def close_cover(self, **kwargs):
"""Send close command."""
self.set_door_state(STATE_CLOSED)
def set_door_state(self, state):
"""Send state command."""
characteristics = [{'aid': self._aid,
'iid': self._chars['door-state.target'],
'value': TARGET_GARAGE_STATE_MAP[state]}]
self.put_characteristics(characteristics)
@property
def device_state_attributes(self):
"""Return the optional state attributes."""
if self._obstruction_detected is None:
return None
return {
'obstruction-detected': self._obstruction_detected,
}
class HomeKitWindowCover(HomeKitEntity, CoverDevice):
"""Representation of a HomeKit Window or Window Covering."""
def __init__(self, accessory, discovery_info):
"""Initialise the Cover."""
super().__init__(accessory, discovery_info)
self._name = None
self._state = None
self._position = None
self._tilt_position = None
self._hold = None
self._obstruction_detected = None
self.lock_state = None
@property
def available(self):
"""Return True if entity is available."""
return self._state is not None
def update_characteristics(self, characteristics):
"""Synchronise the Cover state with Home Assistant."""
# pylint: disable=import-error
from homekit.model.characteristics import CharacteristicsTypes
for characteristic in characteristics:
ctype = characteristic['type']
ctype = CharacteristicsTypes.get_short(ctype)
if ctype == "position.state":
self._chars['position.state'] = \
characteristic['iid']
if 'value' in characteristic:
self._state = \
CURRENT_WINDOW_STATE_MAP[characteristic['value']]
elif ctype == "position.current":
self._chars['position.current'] = \
characteristic['iid']
self._position = characteristic['value']
elif ctype == "position.target":
self._chars['position.target'] = \
characteristic['iid']
elif ctype == "position.hold":
self._chars['position.hold'] = characteristic['iid']
if 'value' in characteristic:
self._hold = characteristic['value']
elif ctype == "vertical-tilt.current":
self._chars['vertical-tilt.current'] = characteristic['iid']
if characteristic['value'] is not None:
self._tilt_position = characteristic['value']
elif ctype == "horizontal-tilt.current":
self._chars['horizontal-tilt.current'] = characteristic['iid']
if characteristic['value'] is not None:
self._tilt_position = characteristic['value']
elif ctype == "vertical-tilt.target":
self._chars['vertical-tilt.target'] = \
characteristic['iid']
elif ctype == "horizontal-tilt.target":
self._chars['vertical-tilt.target'] = \
characteristic['iid']
elif ctype == "obstruction-detected":
self._chars['obstruction-detected'] = characteristic['iid']
self._obstruction_detected = characteristic['value']
elif ctype == "name":
self._chars['name'] = characteristic['iid']
if 'value' in characteristic:
self._name = characteristic['value']
@property
def name(self):
"""Return the name of the cover."""
return self._name
@property
def supported_features(self):
"""Flag supported features."""
supported_features = (
SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION)
if self._tilt_position is not None:
supported_features |= (
SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT |
SUPPORT_SET_TILT_POSITION)
return supported_features
@property
def current_cover_position(self):
"""Return the current position of cover."""
return self._position
@property
def is_closed(self):
"""Return true if cover is closed, else False."""
return self._position == 0
@property
def is_closing(self):
"""Return if the cover is closing or not."""
return self._state == STATE_CLOSING
@property
def is_opening(self):
"""Return if the cover is opening or not."""
return self._state == STATE_OPENING
def open_cover(self, **kwargs):
"""Send open command."""
self.set_cover_position(position=100)
def close_cover(self, **kwargs):
"""Send close command."""
self.set_cover_position(position=0)
def set_cover_position(self, **kwargs):
"""Send position command."""
position = kwargs[ATTR_POSITION]
characteristics = [{'aid': self._aid,
'iid': self._chars['position.target'],
'value': position}]
self.put_characteristics(characteristics)
@property
def current_cover_tilt_position(self):
"""Return current position of cover tilt."""
return self._tilt_position
def set_cover_tilt_position(self, **kwargs):
"""Move the cover tilt to a specific position."""
tilt_position = kwargs[ATTR_TILT_POSITION]
if 'vertical-tilt.target' in self._chars:
characteristics = [{'aid': self._aid,
'iid': self._chars['vertical-tilt.target'],
'value': tilt_position}]
self.put_characteristics(characteristics)
elif 'horizontal-tilt.target' in self._chars:
characteristics = [{'aid': self._aid,
'iid':
self._chars['horizontal-tilt.target'],
'value': tilt_position}]
self.put_characteristics(characteristics)
@property
def device_state_attributes(self):
"""Return the optional state attributes."""
state_attributes = {}
if self._obstruction_detected is not None:
state_attributes['obstruction-detected'] = \
self._obstruction_detected
if self._hold is not None:
state_attributes['hold-position'] = \
self._hold
return state_attributes

View File

@ -46,7 +46,7 @@ COVER_SCHEMA = vol.Schema({
}) })
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_COVERS): vol.Schema({cv.slug: COVER_SCHEMA}), vol.Required(CONF_COVERS): cv.schema_with_slug_keys(COVER_SCHEMA),
}) })

View File

@ -18,7 +18,8 @@ _LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['scsgate'] DEPENDENCIES = ['scsgate']
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_DEVICES): vol.Schema({cv.slug: scsgate.SCSGATE_SCHEMA}), vol.Required(CONF_DEVICES):
cv.schema_with_slug_keys(scsgate.SCSGATE_SCHEMA),
}) })

View File

@ -67,7 +67,7 @@ COVER_SCHEMA = vol.Schema({
}) })
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_COVERS): vol.Schema({cv.slug: COVER_SCHEMA}), vol.Required(CONF_COVERS): cv.schema_with_slug_keys(COVER_SCHEMA),
}) })

View File

@ -26,7 +26,7 @@ COVER_SCHEMA = vol.Schema({
}) })
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_COVERS): vol.Schema({cv.slug: COVER_SCHEMA}), vol.Required(CONF_COVERS): cv.schema_with_slug_keys(COVER_SCHEMA),
}) })
DEPENDENCIES = ['velbus'] DEPENDENCIES = ['velbus']

View File

@ -0,0 +1,19 @@
{
"config": {
"abort": {
"already_configured": "Ger\u00e4t ist bereits konfiguriert",
"device_fail": "Unerwarteter Fehler beim Erstellen des Ger\u00e4ts.",
"device_timeout": "Zeit\u00fcberschreitung beim Verbinden mit dem Ger\u00e4t."
},
"step": {
"user": {
"data": {
"host": "Host"
},
"description": "Geben Sie die IP-Adresse Ihrer Daikin AC ein.",
"title": "Daikin AC konfigurieren"
}
},
"title": "Daikin AC"
}
}

View File

@ -0,0 +1,11 @@
{
"config": {
"step": {
"user": {
"data": {
"host": "Kiszolg\u00e1l\u00f3"
}
}
}
}
}

View File

@ -0,0 +1,19 @@
{
"config": {
"abort": {
"already_configured": "Apparaat is al geconfigureerd",
"device_fail": "Onverwachte fout bij het aanmaken van een apparaat.",
"device_timeout": "Time-out voor verbinding met het apparaat."
},
"step": {
"user": {
"data": {
"host": "Host"
},
"description": "Voer het IP-adres van uw Daikin AC in.",
"title": "Daikin AC instellen"
}
},
"title": "Daikin AC"
}
}

View File

@ -0,0 +1,19 @@
{
"config": {
"abort": {
"already_configured": "Enheten er allerede konfigurert",
"device_fail": "Uventet feil under oppretting av enheten.",
"device_timeout": "Tidsavbrudd for tilkobling til enheten."
},
"step": {
"user": {
"data": {
"host": "Vert"
},
"description": "Angi IP-adressen til din Daikin AC.",
"title": "Konfigurer Daikin AC"
}
},
"title": "Daikin AC"
}
}

View File

@ -0,0 +1,19 @@
{
"config": {
"abort": {
"already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane",
"device_fail": "Nieoczekiwany b\u0142\u0105d tworzenia urz\u0105dzenia.",
"device_timeout": "Limit czasu pod\u0142\u0105czenia do urz\u0105dzenia."
},
"step": {
"user": {
"data": {
"host": "Host"
},
"description": "Wprowad\u017a adres IP Daikin AC.",
"title": "Konfiguracja Daikin AC"
}
},
"title": "Daikin AC"
}
}

View File

@ -0,0 +1,16 @@
{
"config": {
"abort": {
"already_configured": "O dispositivo j\u00e1 est\u00e1 configurado",
"device_fail": "Erro inesperado ao criar dispositivo.",
"device_timeout": "Excedido tempo limite conectando ao dispositivo"
},
"step": {
"user": {
"description": "Digite o endere\u00e7o IP do seu AC Daikin.",
"title": "Configurar o AC Daikin"
}
},
"title": "AC Daikin"
}
}

View File

@ -0,0 +1,19 @@
{
"config": {
"abort": {
"already_configured": "O dispositivo j\u00e1 est\u00e1 configurado",
"device_fail": "Erro inesperado ao criar dispositivo.",
"device_timeout": "Tempo excedido a tentar ligar ao dispositivo."
},
"step": {
"user": {
"data": {
"host": "Servidor"
},
"description": "Introduza o endere\u00e7o IP do seu Daikin AC.",
"title": "Configurar o Daikin AC"
}
},
"title": "Daikin AC"
}
}

View File

@ -12,7 +12,7 @@
"init": { "init": {
"data": { "data": {
"host": "Host", "host": "Host",
"port": "Port (Standartwert : '80')" "port": "Port"
}, },
"title": "Definiere das deCONZ-Gateway" "title": "Definiere das deCONZ-Gateway"
}, },

View File

@ -0,0 +1,13 @@
{
"config": {
"step": {
"init": {
"data": {
"host": "",
"port": ""
}
}
},
"title": "deCONZ Zigbee l\u00fc\u00fcs"
}
}

View File

@ -12,7 +12,7 @@
"init": { "init": {
"data": { "data": {
"host": "Gostitelj", "host": "Gostitelj",
"port": "Vrata (privzeta vrednost: '80')" "port": "Vrata"
}, },
"title": "Dolo\u010dite deCONZ prehod" "title": "Dolo\u010dite deCONZ prehod"
}, },
@ -28,6 +28,6 @@
"title": "Dodatne mo\u017enosti konfiguracije za deCONZ" "title": "Dodatne mo\u017enosti konfiguracije za deCONZ"
} }
}, },
"title": "deCONZ" "title": "deCONZ Zigbee prehod"
} }
} }

View File

@ -0,0 +1,89 @@
"""
Support for deCONZ binary sensor.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.deconz/
"""
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.const import ATTR_BATTERY_LEVEL
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .const import (
ATTR_DARK, ATTR_ON, CONF_ALLOW_CLIP_SENSOR, DOMAIN as DECONZ_DOMAIN)
from .deconz_device import DeconzDevice
DEPENDENCIES = ['deconz']
async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Old way of setting up deCONZ binary sensors."""
pass
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the deCONZ binary sensor."""
gateway = hass.data[DECONZ_DOMAIN]
@callback
def async_add_sensor(sensors):
"""Add binary sensor from deCONZ."""
from pydeconz.sensor import DECONZ_BINARY_SENSOR
entities = []
allow_clip_sensor = config_entry.data.get(CONF_ALLOW_CLIP_SENSOR, True)
for sensor in sensors:
if sensor.type in DECONZ_BINARY_SENSOR and \
not (not allow_clip_sensor and sensor.type.startswith('CLIP')):
entities.append(DeconzBinarySensor(sensor, gateway))
async_add_entities(entities, True)
gateway.listeners.append(
async_dispatcher_connect(hass, 'deconz_new_sensor', async_add_sensor))
async_add_sensor(gateway.api.sensors.values())
class DeconzBinarySensor(DeconzDevice, BinarySensorDevice):
"""Representation of a deCONZ binary sensor."""
@callback
def async_update_callback(self, reason):
"""Update the sensor's state.
If reason is that state is updated,
or reachable has changed or battery has changed.
"""
if reason['state'] or \
'reachable' in reason['attr'] or \
'battery' in reason['attr'] or \
'on' in reason['attr']:
self.async_schedule_update_ha_state()
@property
def is_on(self):
"""Return true if sensor is on."""
return self._device.is_tripped
@property
def device_class(self):
"""Return the class of the sensor."""
return self._device.sensor_class
@property
def icon(self):
"""Return the icon to use in the frontend."""
return self._device.sensor_icon
@property
def device_state_attributes(self):
"""Return the state attributes of the sensor."""
from pydeconz.sensor import PRESENCE
attr = {}
if self._device.battery:
attr[ATTR_BATTERY_LEVEL] = self._device.battery
if self._device.on is not None:
attr[ATTR_ON] = self._device.on
if self._device.type in PRESENCE and self._device.dark is not None:
attr[ATTR_DARK] = self._device.dark
return attr

View File

@ -4,16 +4,15 @@ Support for deCONZ covers.
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/cover.deconz/ https://home-assistant.io/components/cover.deconz/
""" """
from homeassistant.components.deconz.const import (
COVER_TYPES, DAMPERS, DECONZ_REACHABLE, DOMAIN as DECONZ_DOMAIN,
WINDOW_COVERS)
from homeassistant.components.cover import ( from homeassistant.components.cover import (
ATTR_POSITION, CoverDevice, SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_STOP, ATTR_POSITION, CoverDevice, SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_STOP,
SUPPORT_SET_POSITION) SUPPORT_SET_POSITION)
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .const import COVER_TYPES, DAMPERS, DOMAIN as DECONZ_DOMAIN, WINDOW_COVERS
from .deconz_device import DeconzDevice
DEPENDENCIES = ['deconz'] DEPENDENCIES = ['deconz']
ZIGBEE_SPEC = ['lumi.curtain'] ZIGBEE_SPEC = ['lumi.curtain']
@ -50,67 +49,36 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_cover(gateway.api.lights.values()) async_add_cover(gateway.api.lights.values())
class DeconzCover(CoverDevice): class DeconzCover(DeconzDevice, CoverDevice):
"""Representation of a deCONZ cover.""" """Representation of a deCONZ cover."""
def __init__(self, cover, gateway): def __init__(self, device, gateway):
"""Set up cover and add update callback to get data from websocket.""" """Set up cover and add update callback to get data from websocket."""
self._cover = cover super().__init__(device, gateway)
self.gateway = gateway
self.unsub_dispatcher = None
self._features = SUPPORT_OPEN self._features = SUPPORT_OPEN
self._features |= SUPPORT_CLOSE self._features |= SUPPORT_CLOSE
self._features |= SUPPORT_STOP self._features |= SUPPORT_STOP
self._features |= SUPPORT_SET_POSITION self._features |= SUPPORT_SET_POSITION
async def async_added_to_hass(self):
"""Subscribe to covers events."""
self._cover.register_async_callback(self.async_update_callback)
self.gateway.deconz_ids[self.entity_id] = self._cover.deconz_id
self.unsub_dispatcher = async_dispatcher_connect(
self.hass, DECONZ_REACHABLE, self.async_update_callback)
async def async_will_remove_from_hass(self) -> None:
"""Disconnect cover object when removed."""
if self.unsub_dispatcher is not None:
self.unsub_dispatcher()
self._cover.remove_callback(self.async_update_callback)
self._cover = None
@callback
def async_update_callback(self, reason):
"""Update the cover's state."""
self.async_schedule_update_ha_state()
@property @property
def current_cover_position(self): def current_cover_position(self):
"""Return the current position of the cover.""" """Return the current position of the cover."""
if self.is_closed: if self.is_closed:
return 0 return 0
return int(self._cover.brightness / 255 * 100) return int(self._device.brightness / 255 * 100)
@property @property
def is_closed(self): def is_closed(self):
"""Return if the cover is closed.""" """Return if the cover is closed."""
return not self._cover.state return not self._device.state
@property
def name(self):
"""Return the name of the cover."""
return self._cover.name
@property
def unique_id(self):
"""Return a unique identifier for this cover."""
return self._cover.uniqueid
@property @property
def device_class(self): def device_class(self):
"""Return the class of the cover.""" """Return the class of the cover."""
if self._cover.type in DAMPERS: if self._device.type in DAMPERS:
return 'damper' return 'damper'
if self._cover.type in WINDOW_COVERS: if self._device.type in WINDOW_COVERS:
return 'window' return 'window'
@property @property
@ -118,16 +86,6 @@ class DeconzCover(CoverDevice):
"""Flag supported features.""" """Flag supported features."""
return self._features return self._features
@property
def available(self):
"""Return True if light is available."""
return self.gateway.available and self._cover.reachable
@property
def should_poll(self):
"""No polling needed."""
return False
async def async_set_cover_position(self, **kwargs): async def async_set_cover_position(self, **kwargs):
"""Move the cover to a specific position.""" """Move the cover to a specific position."""
position = kwargs[ATTR_POSITION] position = kwargs[ATTR_POSITION]
@ -135,7 +93,7 @@ class DeconzCover(CoverDevice):
if position > 0: if position > 0:
data['on'] = True data['on'] = True
data['bri'] = int(position / 100 * 255) data['bri'] = int(position / 100 * 255)
await self._cover.async_set_state(data) await self._device.async_set_state(data)
async def async_open_cover(self, **kwargs): async def async_open_cover(self, **kwargs):
"""Open cover.""" """Open cover."""
@ -150,25 +108,7 @@ class DeconzCover(CoverDevice):
async def async_stop_cover(self, **kwargs): async def async_stop_cover(self, **kwargs):
"""Stop cover.""" """Stop cover."""
data = {'bri_inc': 0} data = {'bri_inc': 0}
await self._cover.async_set_state(data) await self._device.async_set_state(data)
@property
def device_info(self):
"""Return a device description for device registry."""
if (self._cover.uniqueid is None or
self._cover.uniqueid.count(':') != 7):
return None
serial = self._cover.uniqueid.split('-', 1)[0]
bridgeid = self.gateway.api.config.bridgeid
return {
'connections': {(CONNECTION_ZIGBEE, serial)},
'identifiers': {(DECONZ_DOMAIN, serial)},
'manufacturer': self._cover.manufacturer,
'model': self._cover.modelid,
'name': self._cover.name,
'sw_version': self._cover.swversion,
'via_hub': (DECONZ_DOMAIN, bridgeid),
}
class DeconzCoverZigbeeSpec(DeconzCover): class DeconzCoverZigbeeSpec(DeconzCover):
@ -177,12 +117,12 @@ class DeconzCoverZigbeeSpec(DeconzCover):
@property @property
def current_cover_position(self): def current_cover_position(self):
"""Return the current position of the cover.""" """Return the current position of the cover."""
return 100 - int(self._cover.brightness / 255 * 100) return 100 - int(self._device.brightness / 255 * 100)
@property @property
def is_closed(self): def is_closed(self):
"""Return if the cover is closed.""" """Return if the cover is closed."""
return self._cover.state return self._device.state
async def async_set_cover_position(self, **kwargs): async def async_set_cover_position(self, **kwargs):
"""Move the cover to a specific position.""" """Move the cover to a specific position."""
@ -191,4 +131,4 @@ class DeconzCoverZigbeeSpec(DeconzCover):
if position < 100: if position < 100:
data['on'] = True data['on'] = True
data['bri'] = 255 - int(position / 100 * 255) data['bri'] = 255 - int(position / 100 * 255)
await self._cover.async_set_state(data) await self._device.async_set_state(data)

View File

@ -0,0 +1,74 @@
"""Base class for deCONZ devices."""
from homeassistant.core import callback
from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity
from .const import DECONZ_REACHABLE, DOMAIN as DECONZ_DOMAIN
class DeconzDevice(Entity):
"""Representation of a deCONZ device."""
def __init__(self, device, gateway):
"""Set up device and add update callback to get data from websocket."""
self._device = device
self.gateway = gateway
self.unsub_dispatcher = None
async def async_added_to_hass(self):
"""Subscribe to device events."""
self._device.register_async_callback(self.async_update_callback)
self.gateway.deconz_ids[self.entity_id] = self._device.deconz_id
self.unsub_dispatcher = async_dispatcher_connect(
self.hass, DECONZ_REACHABLE, self.async_update_callback)
async def async_will_remove_from_hass(self) -> None:
"""Disconnect device object when removed."""
if self.unsub_dispatcher is not None:
self.unsub_dispatcher()
self._device.remove_callback(self.async_update_callback)
self._device = None
@callback
def async_update_callback(self, reason):
"""Update the device's state."""
self.async_schedule_update_ha_state()
@property
def name(self):
"""Return the name of the device."""
return self._device.name
@property
def unique_id(self):
"""Return a unique identifier for this device."""
return self._device.uniqueid
@property
def available(self):
"""Return True if device is available."""
return self.gateway.available and self._device.reachable
@property
def should_poll(self):
"""No polling needed."""
return False
@property
def device_info(self):
"""Return a device description for device registry."""
if (self._device.uniqueid is None or
self._device.uniqueid.count(':') != 7):
return None
serial = self._device.uniqueid.split('-', 1)[0]
bridgeid = self.gateway.api.config.bridgeid
return {
'connections': {(CONNECTION_ZIGBEE, serial)},
'identifiers': {(DECONZ_DOMAIN, serial)},
'manufacturer': self._device.manufacturer,
'model': self._device.modelid,
'name': self._device.name,
'sw_version': self._device.swversion,
'via_hub': (DECONZ_DOMAIN, bridgeid),
}

View File

@ -4,19 +4,20 @@ Support for deCONZ light.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/light.deconz/ https://home-assistant.io/components/light.deconz/
""" """
from homeassistant.components.deconz.const import (
CONF_ALLOW_DECONZ_GROUPS, DECONZ_REACHABLE, DOMAIN as DECONZ_DOMAIN,
COVER_TYPES, SWITCH_TYPES)
from homeassistant.components.light import ( from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_HS_COLOR, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_HS_COLOR,
ATTR_TRANSITION, EFFECT_COLORLOOP, FLASH_LONG, FLASH_SHORT, ATTR_TRANSITION, EFFECT_COLORLOOP, FLASH_LONG, FLASH_SHORT,
SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT,
SUPPORT_FLASH, SUPPORT_TRANSITION, Light) SUPPORT_FLASH, SUPPORT_TRANSITION, Light)
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
import homeassistant.util.color as color_util import homeassistant.util.color as color_util
from .const import (
CONF_ALLOW_DECONZ_GROUPS, DOMAIN as DECONZ_DOMAIN, COVER_TYPES,
SWITCH_TYPES)
from .deconz_device import DeconzDevice
DEPENDENCIES = ['deconz'] DEPENDENCIES = ['deconz']
@ -59,51 +60,30 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_group(gateway.api.groups.values()) async_add_group(gateway.api.groups.values())
class DeconzLight(Light): class DeconzLight(DeconzDevice, Light):
"""Representation of a deCONZ light.""" """Representation of a deCONZ light."""
def __init__(self, light, gateway): def __init__(self, device, gateway):
"""Set up light and add update callback to get data from websocket.""" """Set up light and add update callback to get data from websocket."""
self._light = light super().__init__(device, gateway)
self.gateway = gateway
self.unsub_dispatcher = None
self._features = SUPPORT_BRIGHTNESS self._features = SUPPORT_BRIGHTNESS
self._features |= SUPPORT_FLASH self._features |= SUPPORT_FLASH
self._features |= SUPPORT_TRANSITION self._features |= SUPPORT_TRANSITION
if self._light.ct is not None: if self._device.ct is not None:
self._features |= SUPPORT_COLOR_TEMP self._features |= SUPPORT_COLOR_TEMP
if self._light.xy is not None: if self._device.xy is not None:
self._features |= SUPPORT_COLOR self._features |= SUPPORT_COLOR
if self._light.effect is not None: if self._device.effect is not None:
self._features |= SUPPORT_EFFECT self._features |= SUPPORT_EFFECT
async def async_added_to_hass(self):
"""Subscribe to lights events."""
self._light.register_async_callback(self.async_update_callback)
self.gateway.deconz_ids[self.entity_id] = self._light.deconz_id
self.unsub_dispatcher = async_dispatcher_connect(
self.hass, DECONZ_REACHABLE, self.async_update_callback)
async def async_will_remove_from_hass(self) -> None:
"""Disconnect light object when removed."""
if self.unsub_dispatcher is not None:
self.unsub_dispatcher()
self._light.remove_callback(self.async_update_callback)
self._light = None
@callback
def async_update_callback(self, reason):
"""Update the light's state."""
self.async_schedule_update_ha_state()
@property @property
def brightness(self): def brightness(self):
"""Return the brightness of this light between 0..255.""" """Return the brightness of this light between 0..255."""
return self._light.brightness return self._device.brightness
@property @property
def effect_list(self): def effect_list(self):
@ -113,48 +93,28 @@ class DeconzLight(Light):
@property @property
def color_temp(self): def color_temp(self):
"""Return the CT color value.""" """Return the CT color value."""
if self._light.colormode != 'ct': if self._device.colormode != 'ct':
return None return None
return self._light.ct return self._device.ct
@property @property
def hs_color(self): def hs_color(self):
"""Return the hs color value.""" """Return the hs color value."""
if self._light.colormode in ('xy', 'hs') and self._light.xy: if self._device.colormode in ('xy', 'hs') and self._device.xy:
return color_util.color_xy_to_hs(*self._light.xy) return color_util.color_xy_to_hs(*self._device.xy)
return None return None
@property @property
def is_on(self): def is_on(self):
"""Return true if light is on.""" """Return true if light is on."""
return self._light.state return self._device.state
@property
def name(self):
"""Return the name of the light."""
return self._light.name
@property
def unique_id(self):
"""Return a unique identifier for this light."""
return self._light.uniqueid
@property @property
def supported_features(self): def supported_features(self):
"""Flag supported features.""" """Flag supported features."""
return self._features return self._features
@property
def available(self):
"""Return True if light is available."""
return self.gateway.available and self._light.reachable
@property
def should_poll(self):
"""No polling needed."""
return False
async def async_turn_on(self, **kwargs): async def async_turn_on(self, **kwargs):
"""Turn on light.""" """Turn on light."""
data = {'on': True} data = {'on': True}
@ -185,7 +145,7 @@ class DeconzLight(Light):
else: else:
data['effect'] = 'none' data['effect'] = 'none'
await self._light.async_set_state(data) await self._device.async_set_state(data)
async def async_turn_off(self, **kwargs): async def async_turn_off(self, **kwargs):
"""Turn off light.""" """Turn off light."""
@ -203,31 +163,13 @@ class DeconzLight(Light):
data['alert'] = 'lselect' data['alert'] = 'lselect'
del data['on'] del data['on']
await self._light.async_set_state(data) await self._device.async_set_state(data)
@property @property
def device_state_attributes(self): def device_state_attributes(self):
"""Return the device state attributes.""" """Return the device state attributes."""
attributes = {} attributes = {}
attributes['is_deconz_group'] = self._light.type == 'LightGroup' attributes['is_deconz_group'] = self._device.type == 'LightGroup'
if self._light.type == 'LightGroup': if self._device.type == 'LightGroup':
attributes['all_on'] = self._light.all_on attributes['all_on'] = self._device.all_on
return attributes return attributes
@property
def device_info(self):
"""Return a device description for device registry."""
if (self._light.uniqueid is None or
self._light.uniqueid.count(':') != 7):
return None
serial = self._light.uniqueid.split('-', 1)[0]
bridgeid = self.gateway.api.config.bridgeid
return {
'connections': {(CONNECTION_ZIGBEE, serial)},
'identifiers': {(DECONZ_DOMAIN, serial)},
'manufacturer': self._light.manufacturer,
'model': self._light.modelid,
'name': self._light.name,
'sw_version': self._light.swversion,
'via_hub': (DECONZ_DOMAIN, bridgeid),
}

View File

@ -0,0 +1,153 @@
"""
Support for deCONZ sensor.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/sensor.deconz/
"""
from homeassistant.const import (
ATTR_BATTERY_LEVEL, ATTR_VOLTAGE, DEVICE_CLASS_BATTERY)
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.util import slugify
from .const import (
ATTR_DARK, ATTR_ON, CONF_ALLOW_CLIP_SENSOR, DOMAIN as DECONZ_DOMAIN)
from .deconz_device import DeconzDevice
DEPENDENCIES = ['deconz']
ATTR_CURRENT = 'current'
ATTR_DAYLIGHT = 'daylight'
ATTR_EVENT_ID = 'event_id'
async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Old way of setting up deCONZ sensors."""
pass
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the deCONZ sensors."""
gateway = hass.data[DECONZ_DOMAIN]
@callback
def async_add_sensor(sensors):
"""Add sensors from deCONZ."""
from pydeconz.sensor import DECONZ_SENSOR, SWITCH as DECONZ_REMOTE
entities = []
allow_clip_sensor = config_entry.data.get(CONF_ALLOW_CLIP_SENSOR, True)
for sensor in sensors:
if sensor.type in DECONZ_SENSOR and \
not (not allow_clip_sensor and sensor.type.startswith('CLIP')):
if sensor.type in DECONZ_REMOTE:
if sensor.battery:
entities.append(DeconzBattery(sensor, gateway))
else:
entities.append(DeconzSensor(sensor, gateway))
async_add_entities(entities, True)
gateway.listeners.append(
async_dispatcher_connect(hass, 'deconz_new_sensor', async_add_sensor))
async_add_sensor(gateway.api.sensors.values())
class DeconzSensor(DeconzDevice):
"""Representation of a deCONZ sensor."""
@callback
def async_update_callback(self, reason):
"""Update the sensor's state.
If reason is that state is updated,
or reachable has changed or battery has changed.
"""
if reason['state'] or \
'reachable' in reason['attr'] or \
'battery' in reason['attr'] or \
'on' in reason['attr']:
self.async_schedule_update_ha_state()
@property
def state(self):
"""Return the state of the sensor."""
return self._device.state
@property
def device_class(self):
"""Return the class of the sensor."""
return self._device.sensor_class
@property
def icon(self):
"""Return the icon to use in the frontend."""
return self._device.sensor_icon
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this sensor."""
return self._device.sensor_unit
@property
def device_state_attributes(self):
"""Return the state attributes of the sensor."""
from pydeconz.sensor import LIGHTLEVEL
attr = {}
if self._device.battery:
attr[ATTR_BATTERY_LEVEL] = self._device.battery
if self._device.on is not None:
attr[ATTR_ON] = self._device.on
if self._device.type in LIGHTLEVEL and self._device.dark is not None:
attr[ATTR_DARK] = self._device.dark
if self.unit_of_measurement == 'Watts':
attr[ATTR_CURRENT] = self._device.current
attr[ATTR_VOLTAGE] = self._device.voltage
if self._device.sensor_class == 'daylight':
attr[ATTR_DAYLIGHT] = self._device.daylight
return attr
class DeconzBattery(DeconzDevice):
"""Battery class for when a device is only represented as an event."""
def __init__(self, device, gateway):
"""Register dispatcher callback for update of battery state."""
super().__init__(device, gateway)
self._name = '{} {}'.format(self._device.name, 'Battery Level')
self._unit_of_measurement = "%"
@callback
def async_update_callback(self, reason):
"""Update the battery's state, if needed."""
if 'reachable' in reason['attr'] or 'battery' in reason['attr']:
self.async_schedule_update_ha_state()
@property
def state(self):
"""Return the state of the battery."""
return self._device.battery
@property
def name(self):
"""Return the name of the battery."""
return self._name
@property
def device_class(self):
"""Return the class of the sensor."""
return DEVICE_CLASS_BATTERY
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this entity."""
return self._unit_of_measurement
@property
def device_state_attributes(self):
"""Return the state attributes of the battery."""
attr = {
ATTR_EVENT_ID: slugify(self._device.name),
}
return attr

View File

@ -0,0 +1,83 @@
"""
Support for deCONZ switches.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/switch.deconz/
"""
from homeassistant.components.switch import SwitchDevice
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .const import DOMAIN as DECONZ_DOMAIN, POWER_PLUGS, SIRENS
from .deconz_device import DeconzDevice
DEPENDENCIES = ['deconz']
async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Old way of setting up deCONZ switches."""
pass
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up switches for deCONZ component.
Switches are based same device class as lights in deCONZ.
"""
gateway = hass.data[DECONZ_DOMAIN]
@callback
def async_add_switch(lights):
"""Add switch from deCONZ."""
entities = []
for light in lights:
if light.type in POWER_PLUGS:
entities.append(DeconzPowerPlug(light, gateway))
elif light.type in SIRENS:
entities.append(DeconzSiren(light, gateway))
async_add_entities(entities, True)
gateway.listeners.append(
async_dispatcher_connect(hass, 'deconz_new_light', async_add_switch))
async_add_switch(gateway.api.lights.values())
class DeconzPowerPlug(DeconzDevice, SwitchDevice):
"""Representation of a deCONZ power plug."""
@property
def is_on(self):
"""Return true if switch is on."""
return self._device.state
async def async_turn_on(self, **kwargs):
"""Turn on switch."""
data = {'on': True}
await self._device.async_set_state(data)
async def async_turn_off(self, **kwargs):
"""Turn off switch."""
data = {'on': False}
await self._device.async_set_state(data)
class DeconzSiren(DeconzDevice, SwitchDevice):
"""Representation of a deCONZ siren."""
@property
def is_on(self):
"""Return true if switch is on."""
return self._device.alert == 'lselect'
async def async_turn_on(self, **kwargs):
"""Turn on switch."""
data = {'alert': 'lselect'}
await self._device.async_set_state(data)
async def async_turn_off(self, **kwargs):
"""Turn off switch."""
data = {'alert': 'none'}
await self._device.async_set_state(data)

View File

@ -15,7 +15,7 @@ import homeassistant.util.dt as dt_util
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['pygatt==3.2.0'] REQUIREMENTS = ['pygatt[GATTTOOL]==3.2.0']
BLE_PREFIX = 'BLE_' BLE_PREFIX = 'BLE_'
MIN_SEEN_NEW = 5 MIN_SEEN_NEW = 5

View File

@ -19,7 +19,7 @@ from homeassistant.helpers.event import track_time_interval
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from homeassistant.util import slugify, dt as dt_util from homeassistant.util import slugify, dt as dt_util
REQUIREMENTS = ['locationsharinglib==3.0.9'] REQUIREMENTS = ['locationsharinglib==3.0.11']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -89,5 +89,7 @@ class GoogleHomeDeviceScanner(DeviceScanner):
devices[uuid]['btle_mac_address'] = device['mac_address'] devices[uuid]['btle_mac_address'] = device['mac_address']
devices[uuid]['ghname'] = ghname devices[uuid]['ghname'] = ghname
devices[uuid]['source_type'] = 'bluetooth' devices[uuid]['source_type'] = 'bluetooth'
if device['name']:
devices[uuid]['btle_name'] = device['name']
await self.scanner.clear_scan_result() await self.scanner.clear_scan_result()
self.last_results = devices self.last_results = devices

View File

@ -5,104 +5,28 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.gpslogger/ https://home-assistant.io/components/device_tracker.gpslogger/
""" """
import logging import logging
from hmac import compare_digest
from aiohttp.web import Request, HTTPUnauthorized from homeassistant.components.gpslogger import TRACKER_UPDATE
import voluptuous as vol from homeassistant.helpers.dispatcher import async_dispatcher_connect
import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
CONF_PASSWORD, HTTP_UNPROCESSABLE_ENTITY
)
from homeassistant.components.http import (
CONF_API_PASSWORD, HomeAssistantView
)
# pylint: disable=unused-import
from homeassistant.components.device_tracker import ( # NOQA
DOMAIN, PLATFORM_SCHEMA
)
from homeassistant.helpers.typing import HomeAssistantType, ConfigType from homeassistant.helpers.typing import HomeAssistantType, ConfigType
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['http'] DEPENDENCIES = ['gpslogger']
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_PASSWORD): cv.string,
})
async def async_setup_scanner(hass: HomeAssistantType, config: ConfigType, async def async_setup_scanner(hass: HomeAssistantType, config: ConfigType,
async_see, discovery_info=None): async_see, discovery_info=None):
"""Set up an endpoint for the GPSLogger application.""" """Set up an endpoint for the GPSLogger device tracker."""
hass.http.register_view(GPSLoggerView(async_see, config)) async def _set_location(device, gps_location, battery, accuracy, attrs):
"""Fire HA event to set location."""
return True await async_see(
class GPSLoggerView(HomeAssistantView):
"""View to handle GPSLogger requests."""
url = '/api/gpslogger'
name = 'api:gpslogger'
def __init__(self, async_see, config):
"""Initialize GPSLogger url endpoints."""
self.async_see = async_see
self._password = config.get(CONF_PASSWORD)
# this component does not require external authentication if
# password is set
self.requires_auth = self._password is None
async def get(self, request: Request):
"""Handle for GPSLogger message received as GET."""
hass = request.app['hass']
data = request.query
if self._password is not None:
authenticated = CONF_API_PASSWORD in data and compare_digest(
self._password,
data[CONF_API_PASSWORD]
)
if not authenticated:
raise HTTPUnauthorized()
if 'latitude' not in data or 'longitude' not in data:
return ('Latitude and longitude not specified.',
HTTP_UNPROCESSABLE_ENTITY)
if 'device' not in data:
_LOGGER.error("Device id not specified")
return ('Device id not specified.',
HTTP_UNPROCESSABLE_ENTITY)
device = data['device'].replace('-', '')
gps_location = (data['latitude'], data['longitude'])
accuracy = 200
battery = -1
if 'accuracy' in data:
accuracy = int(float(data['accuracy']))
if 'battery' in data:
battery = float(data['battery'])
attrs = {}
if 'speed' in data:
attrs['speed'] = float(data['speed'])
if 'direction' in data:
attrs['direction'] = float(data['direction'])
if 'altitude' in data:
attrs['altitude'] = float(data['altitude'])
if 'provider' in data:
attrs['provider'] = data['provider']
if 'activity' in data:
attrs['activity'] = data['activity']
hass.async_create_task(self.async_see(
dev_id=device, dev_id=device,
gps=gps_location, battery=battery, gps=gps_location,
battery=battery,
gps_accuracy=accuracy, gps_accuracy=accuracy,
attributes=attrs attributes=attrs
)) )
return 'Setting location for {}'.format(device) async_dispatcher_connect(hass, TRACKER_UPDATE, _set_location)
return True

View File

@ -19,7 +19,7 @@ from homeassistant.const import (
INTERFACES = 2 INTERFACES = 2
DEFAULT_TIMEOUT = 10 DEFAULT_TIMEOUT = 10
REQUIREMENTS = ['beautifulsoup4==4.6.3'] REQUIREMENTS = ['beautifulsoup4==4.7.1']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -81,13 +81,14 @@ class LinksysAPDeviceScanner(DeviceScanner):
request = self._make_request(interface) request = self._make_request(interface)
self.last_results.extend( self.last_results.extend(
[x.find_all('td')[1].text [x.find_all('td')[1].text
for x in BS(request.content, "html.parser") for x in BS(request.content, 'html.parser')
.find_all(class_='section-row')] .find_all(class_='section-row')]
) )
return True return True
def _make_request(self, unit=0): def _make_request(self, unit=0):
"""Create a request to get the data."""
# No, the '&&' is not a typo - this is expected by the web interface. # No, the '&&' is not a typo - this is expected by the web interface.
login = base64.b64encode(bytes(self.username, 'utf8')).decode('ascii') login = base64.b64encode(bytes(self.username, 'utf8')).decode('ascii')
pwd = base64.b64encode(bytes(self.password, 'utf8')).decode('ascii') pwd = base64.b64encode(bytes(self.password, 'utf8')).decode('ascii')

View File

@ -4,106 +4,25 @@ Support for the Locative platform.
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/device_tracker.locative/ https://home-assistant.io/components/device_tracker.locative/
""" """
from functools import partial
import logging import logging
from homeassistant.const import ( from homeassistant.components.locative import TRACKER_UPDATE
ATTR_LATITUDE, ATTR_LONGITUDE, STATE_NOT_HOME, HTTP_UNPROCESSABLE_ENTITY) from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.components.http import HomeAssistantView
# pylint: disable=unused-import
from homeassistant.components.device_tracker import ( # NOQA
DOMAIN, PLATFORM_SCHEMA)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['http'] DEPENDENCIES = ['locative']
URL = '/api/locative'
def setup_scanner(hass, config, see, discovery_info=None): async def async_setup_scanner(hass, config, async_see, discovery_info=None):
"""Set up an endpoint for the Locative application.""" """Set up an endpoint for the Locative device tracker."""
hass.http.register_view(LocativeView(see)) async def _set_location(device, gps_location, location_name):
"""Fire HA event to set location."""
await async_see(
dev_id=device,
gps=gps_location,
location_name=location_name
)
async_dispatcher_connect(hass, TRACKER_UPDATE, _set_location)
return True return True
class LocativeView(HomeAssistantView):
"""View to handle Locative requests."""
url = URL
name = 'api:locative'
def __init__(self, see):
"""Initialize Locative URL endpoints."""
self.see = see
async def get(self, request):
"""Locative message received as GET."""
res = await self._handle(request.app['hass'], request.query)
return res
async def post(self, request):
"""Locative message received."""
data = await request.post()
res = await self._handle(request.app['hass'], data)
return res
async def _handle(self, hass, data):
"""Handle locative request."""
if 'latitude' not in data or 'longitude' not in data:
return ('Latitude and longitude not specified.',
HTTP_UNPROCESSABLE_ENTITY)
if 'device' not in data:
_LOGGER.error('Device id not specified.')
return ('Device id not specified.',
HTTP_UNPROCESSABLE_ENTITY)
if 'trigger' not in data:
_LOGGER.error('Trigger is not specified.')
return ('Trigger is not specified.',
HTTP_UNPROCESSABLE_ENTITY)
if 'id' not in data and data['trigger'] != 'test':
_LOGGER.error('Location id not specified.')
return ('Location id not specified.',
HTTP_UNPROCESSABLE_ENTITY)
device = data['device'].replace('-', '')
location_name = data.get('id', data['trigger']).lower()
direction = data['trigger']
gps_location = (data[ATTR_LATITUDE], data[ATTR_LONGITUDE])
if direction == 'enter':
await hass.async_add_job(
partial(self.see, dev_id=device, location_name=location_name,
gps=gps_location))
return 'Setting location to {}'.format(location_name)
if direction == 'exit':
current_state = hass.states.get(
'{}.{}'.format(DOMAIN, device))
if current_state is None or current_state.state == location_name:
location_name = STATE_NOT_HOME
await hass.async_add_job(
partial(self.see, dev_id=device,
location_name=location_name, gps=gps_location))
return 'Setting location to not home'
# Ignore the message if it is telling us to exit a zone that we
# aren't currently in. This occurs when a zone is entered
# before the previous zone was exited. The enter message will
# be sent first, then the exit message will be sent second.
return 'Ignoring exit from {} (already in {})'.format(
location_name, current_state)
if direction == 'test':
# In the app, a test message can be sent. Just return something to
# the user to let them know that it works.
return 'Received test message.'
_LOGGER.error('Received unidentified message from Locative: %s',
direction)
return ('Received unidentified message: {}'.format(direction),
HTTP_UNPROCESSABLE_ENTITY)

View File

@ -14,7 +14,7 @@ async def async_setup_scanner(hass, config, async_see, discovery_info=None):
"""Set up the MySensors device scanner.""" """Set up the MySensors device scanner."""
new_devices = mysensors.setup_mysensors_platform( new_devices = mysensors.setup_mysensors_platform(
hass, DOMAIN, discovery_info, MySensorsDeviceScanner, hass, DOMAIN, discovery_info, MySensorsDeviceScanner,
device_args=(async_see, )) device_args=(hass, async_see))
if not new_devices: if not new_devices:
return False return False
@ -37,12 +37,13 @@ async def async_setup_scanner(hass, config, async_see, discovery_info=None):
class MySensorsDeviceScanner(mysensors.device.MySensorsDevice): class MySensorsDeviceScanner(mysensors.device.MySensorsDevice):
"""Represent a MySensors scanner.""" """Represent a MySensors scanner."""
def __init__(self, async_see, *args): def __init__(self, hass, async_see, *args):
"""Set up instance.""" """Set up instance."""
super().__init__(*args) super().__init__(*args)
self.async_see = async_see self.async_see = async_see
self.hass = hass
async def async_update_callback(self): async def _async_update_callback(self):
"""Update the device.""" """Update the device."""
await self.async_update() await self.async_update()
node = self.gateway.sensors[self.node_id] node = self.gateway.sensors[self.node_id]

View File

@ -14,7 +14,7 @@ from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner) DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST from homeassistant.const import CONF_HOST
REQUIREMENTS = ['pysnmp==4.4.6'] REQUIREMENTS = ['pysnmp==4.4.8']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View File

@ -15,7 +15,7 @@ from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD
from homeassistant.const import CONF_VERIFY_SSL, CONF_MONITORED_CONDITIONS from homeassistant.const import CONF_VERIFY_SSL, CONF_MONITORED_CONDITIONS
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
REQUIREMENTS = ['pyunifi==2.13'] REQUIREMENTS = ['pyunifi==2.16']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_PORT = 'port' CONF_PORT = 'port'

View File

@ -131,6 +131,6 @@ def _response_to_json(response):
active_clients[client.get("mac")] = client active_clients[client.get("mac")] = client
return active_clients return active_clients
except ValueError: except (ValueError, TypeError):
_LOGGER.error("Failed to decode response from AP.") _LOGGER.error("Failed to decode response from AP.")
return {} return {}

View File

@ -10,7 +10,7 @@
"step": { "step": {
"user": { "user": {
"description": "Est\u00e0s segur que vols configurar Dialogflow?", "description": "Est\u00e0s segur que vols configurar Dialogflow?",
"title": "Configuraci\u00f3 del Webhook de Dialogflow" "title": "Configuraci\u00f3 del Webhook Dialogflow"
} }
}, },
"title": "Dialogflow" "title": "Dialogflow"

View File

@ -1,7 +1,15 @@
{ {
"config": { "config": {
"abort": {
"not_internet_accessible": "Ihre Home Assistant-Instanz muss \u00fcber das Internet erreichbar sein, um Dialogflow-Nachrichten empfangen zu k\u00f6nnen.",
"one_instance_allowed": "Nur eine einzige Instanz ist notwendig."
},
"create_entry": {
"default": "Um Ereignisse an den Home Assistant zu senden, m\u00fcssen Sie [Webhook-Integration von Dialogflow]({dialogflow_url}) einrichten. \n\nF\u00fcllen Sie die folgenden Informationen aus: \n\n - URL: ` {webhook_url} ` \n - Methode: POST \n - Inhaltstyp: application / json \n\nWeitere Informationen finden Sie in der [Dokumentation]({docs_url})."
},
"step": { "step": {
"user": { "user": {
"description": "M\u00f6chten Sie Dialogflow wirklich einrichten?",
"title": "Dialogflow Webhook einrichten" "title": "Dialogflow Webhook einrichten"
} }
}, },

View File

@ -4,6 +4,9 @@
"not_internet_accessible": "Su instancia de Home Assistant debe ser accesible desde Internet para recibir mensajes de Dialogflow.", "not_internet_accessible": "Su instancia de Home Assistant debe ser accesible desde Internet para recibir mensajes de Dialogflow.",
"one_instance_allowed": "Solo una instancia es necesaria." "one_instance_allowed": "Solo una instancia es necesaria."
}, },
"create_entry": {
"default": "Para enviar eventos a Home Assistant, necesitas configurar [Integracion de flujos de dialogo de webhook]({dialogflow_url}).\n\nRellena la siguiente informaci\u00f3n:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nVer [Documentaci\u00f3n]({docs_url}) para mas detalles."
},
"step": { "step": {
"user": { "user": {
"description": "\u00bfEst\u00e1 seguro de que desea configurar Dialogflow?" "description": "\u00bfEst\u00e1 seguro de que desea configurar Dialogflow?"

View File

@ -4,6 +4,9 @@
"not_internet_accessible": "Uw Home Assistant instantie moet toegankelijk zijn vanaf het internet om Dialogflow-berichten te ontvangen.", "not_internet_accessible": "Uw Home Assistant instantie moet toegankelijk zijn vanaf het internet om Dialogflow-berichten te ontvangen.",
"one_instance_allowed": "Slechts \u00e9\u00e9n instantie is nodig." "one_instance_allowed": "Slechts \u00e9\u00e9n instantie is nodig."
}, },
"create_entry": {
"default": "Om evenementen naar de Home Assistant te verzenden, moet u [webhookintegratie van Dialogflow]({dialogflow_url}) instellen. \n\n Vul de volgende info in: \n\n - URL: `{webhook_url}` \n - Method: POST \n - Content Type: application/json \n\nZie [de documentatie]({docs_url}) voor verdere informatie."
},
"step": { "step": {
"user": { "user": {
"description": "Weet u zeker dat u Dialogflow wilt instellen?", "description": "Weet u zeker dat u Dialogflow wilt instellen?",

View File

@ -47,6 +47,7 @@ SERVICE_OCTOPRINT = 'octoprint'
SERVICE_FREEBOX = 'freebox' SERVICE_FREEBOX = 'freebox'
SERVICE_IGD = 'igd' SERVICE_IGD = 'igd'
SERVICE_DLNA_DMR = 'dlna_dmr' SERVICE_DLNA_DMR = 'dlna_dmr'
SERVICE_ROKU = 'roku'
CONFIG_ENTRY_HANDLERS = { CONFIG_ENTRY_HANDLERS = {
SERVICE_DAIKIN: 'daikin', SERVICE_DAIKIN: 'daikin',
@ -67,6 +68,7 @@ SERVICE_HANDLERS = {
SERVICE_HASSIO: ('hassio', None), SERVICE_HASSIO: ('hassio', None),
SERVICE_AXIS: ('axis', None), SERVICE_AXIS: ('axis', None),
SERVICE_APPLE_TV: ('apple_tv', None), SERVICE_APPLE_TV: ('apple_tv', None),
SERVICE_ROKU: ('roku', None),
SERVICE_WINK: ('wink', None), SERVICE_WINK: ('wink', None),
SERVICE_XIAOMI_GW: ('xiaomi_aqara', None), SERVICE_XIAOMI_GW: ('xiaomi_aqara', None),
SERVICE_SABNZBD: ('sabnzbd', None), SERVICE_SABNZBD: ('sabnzbd', None),
@ -76,7 +78,6 @@ SERVICE_HANDLERS = {
SERVICE_FREEBOX: ('freebox', None), SERVICE_FREEBOX: ('freebox', None),
'panasonic_viera': ('media_player', 'panasonic_viera'), 'panasonic_viera': ('media_player', 'panasonic_viera'),
'plex_mediaserver': ('media_player', 'plex'), 'plex_mediaserver': ('media_player', 'plex'),
'roku': ('media_player', 'roku'),
'yamaha': ('media_player', 'yamaha'), 'yamaha': ('media_player', 'yamaha'),
'logitech_mediaserver': ('media_player', 'squeezebox'), 'logitech_mediaserver': ('media_player', 'squeezebox'),
'directv': ('media_player', 'directv'), 'directv': ('media_player', 'directv'),

View File

@ -6,15 +6,16 @@ https://home-assistant.io/components/doorbird/
""" """
import logging import logging
from urllib.error import HTTPError
import voluptuous as vol import voluptuous as vol
from homeassistant.components.http import HomeAssistantView from homeassistant.components.http import HomeAssistantView
from homeassistant.const import CONF_HOST, CONF_USERNAME, \ from homeassistant.const import CONF_HOST, CONF_USERNAME, \
CONF_PASSWORD, CONF_NAME, CONF_DEVICES, CONF_MONITORED_CONDITIONS CONF_PASSWORD, CONF_NAME, CONF_DEVICES, CONF_MONITORED_CONDITIONS
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.util import slugify from homeassistant.util import slugify, dt as dt_util
REQUIREMENTS = ['doorbirdpy==2.0.4'] REQUIREMENTS = ['doorbirdpy==2.0.6']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -25,6 +26,7 @@ API_URL = '/api/{}'.format(DOMAIN)
CONF_CUSTOM_URL = 'hass_url_override' CONF_CUSTOM_URL = 'hass_url_override'
CONF_DOORBELL_EVENTS = 'doorbell_events' CONF_DOORBELL_EVENTS = 'doorbell_events'
CONF_DOORBELL_NUMS = 'doorbell_numbers' CONF_DOORBELL_NUMS = 'doorbell_numbers'
CONF_RELAY_NUMS = 'relay_numbers'
CONF_MOTION_EVENTS = 'motion_events' CONF_MOTION_EVENTS = 'motion_events'
CONF_TOKEN = 'token' CONF_TOKEN = 'token'
@ -37,6 +39,10 @@ SENSOR_TYPES = {
'name': 'Motion', 'name': 'Motion',
'device_class': 'motion', 'device_class': 'motion',
}, },
'relay': {
'name': 'Relay',
'device_class': 'relay',
}
} }
RESET_DEVICE_FAVORITES = 'doorbird_reset_favorites' RESET_DEVICE_FAVORITES = 'doorbird_reset_favorites'
@ -47,6 +53,8 @@ DEVICE_SCHEMA = vol.Schema({
vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_DOORBELL_NUMS, default=[1]): vol.All( vol.Optional(CONF_DOORBELL_NUMS, default=[1]): vol.All(
cv.ensure_list, [cv.positive_int]), cv.ensure_list, [cv.positive_int]),
vol.Optional(CONF_RELAY_NUMS, default=[1]): vol.All(
cv.ensure_list, [cv.positive_int]),
vol.Optional(CONF_CUSTOM_URL): cv.string, vol.Optional(CONF_CUSTOM_URL): cv.string,
vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_MONITORED_CONDITIONS, default=[]): vol.Optional(CONF_MONITORED_CONDITIONS, default=[]):
@ -80,6 +88,7 @@ def setup(hass, config):
username = doorstation_config.get(CONF_USERNAME) username = doorstation_config.get(CONF_USERNAME)
password = doorstation_config.get(CONF_PASSWORD) password = doorstation_config.get(CONF_PASSWORD)
doorbell_nums = doorstation_config.get(CONF_DOORBELL_NUMS) doorbell_nums = doorstation_config.get(CONF_DOORBELL_NUMS)
relay_nums = doorstation_config.get(CONF_RELAY_NUMS)
custom_url = doorstation_config.get(CONF_CUSTOM_URL) custom_url = doorstation_config.get(CONF_CUSTOM_URL)
events = doorstation_config.get(CONF_MONITORED_CONDITIONS) events = doorstation_config.get(CONF_MONITORED_CONDITIONS)
name = (doorstation_config.get(CONF_NAME) name = (doorstation_config.get(CONF_NAME)
@ -90,7 +99,7 @@ def setup(hass, config):
if status[0]: if status[0]:
doorstation = ConfiguredDoorBird(device, name, events, custom_url, doorstation = ConfiguredDoorBird(device, name, events, custom_url,
doorbell_nums, token) doorbell_nums, relay_nums, token)
doorstations.append(doorstation) doorstations.append(doorstation)
_LOGGER.info('Connected to DoorBird "%s" as %s@%s', _LOGGER.info('Connected to DoorBird "%s" as %s@%s',
doorstation.name, username, device_ip) doorstation.name, username, device_ip)
@ -105,7 +114,18 @@ def setup(hass, config):
# Subscribe to doorbell or motion events # Subscribe to doorbell or motion events
if events: if events:
doorstation.update_schedule(hass) try:
doorstation.update_schedule(hass)
except HTTPError:
hass.components.persistent_notification.create(
'Doorbird configuration failed. Please verify that API '
'Operator permission is enabled for the Doorbird user. '
'A restart will be required once permissions have been '
'verified.',
title='Doorbird Configuration Failure',
notification_id='doorbird_schedule_error')
return False
hass.data[DOMAIN] = doorstations hass.data[DOMAIN] = doorstations
@ -148,13 +168,15 @@ def handle_event(event):
class ConfiguredDoorBird(): class ConfiguredDoorBird():
"""Attach additional information to pass along with configured device.""" """Attach additional information to pass along with configured device."""
def __init__(self, device, name, events, custom_url, doorbell_nums, token): def __init__(self, device, name, events, custom_url, doorbell_nums,
relay_nums, token):
"""Initialize configured device.""" """Initialize configured device."""
self._name = name self._name = name
self._device = device self._device = device
self._custom_url = custom_url self._custom_url = custom_url
self._monitored_events = events self._monitored_events = events
self._doorbell_nums = doorbell_nums self._doorbell_nums = doorbell_nums
self._relay_nums = relay_nums
self._token = token self._token = token
@property @property
@ -218,9 +240,9 @@ class ConfiguredDoorBird():
# Register HA URL as webhook if not already, then get the ID # Register HA URL as webhook if not already, then get the ID
if not self.webhook_is_registered(hass_url): if not self.webhook_is_registered(hass_url):
self.device.change_favorite('http', self.device.change_favorite('http', 'Home Assistant ({} events)'
'Home Assistant on {} ({} events)' .format(event), hass_url)
.format(hass_url, event), hass_url)
fav_id = self.get_webhook_id(hass_url) fav_id = self.get_webhook_id(hass_url)
if not fav_id: if not fav_id:
@ -239,6 +261,11 @@ class ConfiguredDoorBird():
entry = self.device.get_schedule_entry(event, str(doorbell)) entry = self.device.get_schedule_entry(event, str(doorbell))
entry.output.append(output) entry.output.append(output)
self.device.change_schedule(entry) self.device.change_schedule(entry)
elif event == 'relay':
# Repeat edit for each monitored doorbell number
for relay in self._relay_nums:
entry = self.device.get_schedule_entry(event, str(relay))
entry.output.append(output)
else: else:
entry = self.device.get_schedule_entry(event) entry = self.device.get_schedule_entry(event)
entry.output.append(output) entry.output.append(output)
@ -303,6 +330,16 @@ class ConfiguredDoorBird():
return None return None
def get_event_data(self):
"""Get data to pass along with HA event."""
return {
'timestamp': dt_util.utcnow().isoformat(),
'live_video_url': self._device.live_video_url,
'live_image_url': self._device.live_image_url,
'rtsp_live_video_url': self._device.rtsp_live_video_url,
'html5_viewer_url': self._device.html5_viewer_url
}
class DoorBirdRequestView(HomeAssistantView): class DoorBirdRequestView(HomeAssistantView):
"""Provide a page for the device to call.""" """Provide a page for the device to call."""
@ -330,7 +367,14 @@ class DoorBirdRequestView(HomeAssistantView):
if request_token == '' or not authenticated: if request_token == '' or not authenticated:
return web.Response(status=401, text='Unauthorized') return web.Response(status=401, text='Unauthorized')
hass.bus.async_fire('{}_{}'.format(DOMAIN, sensor)) doorstation = get_doorstation_by_slug(hass, sensor)
if doorstation:
event_data = doorstation.get_event_data()
else:
event_data = {}
hass.bus.async_fire('{}_{}'.format(DOMAIN, sensor), event_data)
return web.Response(status=200, text='OK') return web.Response(status=200, text='OK')

View File

@ -0,0 +1,21 @@
{
"config": {
"abort": {
"name_exists": "El nom ja existeix"
},
"step": {
"user": {
"data": {
"advertise_ip": "IP d'advert\u00e8ncies",
"advertise_port": "Port d'advert\u00e8ncies",
"host_ip": "IP de l'amfitri\u00f3",
"listen_port": "Port d'escolta",
"name": "Nom",
"upnp_bind_multicast": "Enlla\u00e7ar multicast (true/false)"
},
"title": "Configuraci\u00f3 del servidor"
}
},
"title": "EmulatedRoku"
}
}

View File

@ -0,0 +1,21 @@
{
"config": {
"abort": {
"name_exists": "Name already exists"
},
"step": {
"user": {
"data": {
"advertise_ip": "Advertise IP",
"advertise_port": "Advertise port",
"host_ip": "Host IP",
"listen_port": "Listen port",
"name": "Name",
"upnp_bind_multicast": "Bind multicast (True/False)"
},
"title": "Define server configuration"
}
},
"title": "EmulatedRoku"
}
}

View File

@ -0,0 +1,13 @@
{
"config": {
"step": {
"user": {
"data": {
"host_ip": "",
"name": "Nimi"
}
}
},
"title": ""
}
}

View File

@ -0,0 +1,21 @@
{
"config": {
"abort": {
"name_exists": "\uc774\ub984\uc774 \uc774\ubbf8 \uc874\uc7ac\ud569\ub2c8\ub2e4"
},
"step": {
"user": {
"data": {
"advertise_ip": "\uad11\uace0 IP",
"advertise_port": "\uad11\uace0 \ud3ec\ud2b8",
"host_ip": "\ud638\uc2a4\ud2b8 IP",
"listen_port": "\uc218\uc2e0 \ud3ec\ud2b8",
"name": "\uc774\ub984",
"upnp_bind_multicast": "\uba40\ud2f0 \uce90\uc2a4\ud2b8 \ubc14\uc778\ub4dc (\ucc38/\uac70\uc9d3)"
},
"title": "\uc11c\ubc84 \uad6c\uc131 \uc815\uc758"
}
},
"title": "EmulatedRoku"
}
}

View File

@ -0,0 +1,21 @@
{
"config": {
"abort": {
"name_exists": "Navnet eksisterer allerede"
},
"step": {
"user": {
"data": {
"advertise_ip": "Annonser IP",
"advertise_port": "Annonser port",
"host_ip": "Vert IP",
"listen_port": "Lytte port",
"name": "Navn",
"upnp_bind_multicast": "Bind multicast (True/False)"
},
"title": "Definer serverkonfigurasjon"
}
},
"title": "EmulatedRoku"
}
}

View File

@ -0,0 +1,18 @@
{
"config": {
"abort": {
"name_exists": "\u0418\u043c\u044f \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442"
},
"step": {
"user": {
"data": {
"host_ip": "\u0425\u043e\u0441\u0442",
"listen_port": "\u041f\u043e\u0440\u0442",
"name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435"
},
"title": "EmulatedRoku"
}
},
"title": "EmulatedRoku"
}
}

View File

@ -0,0 +1,84 @@
"""
Support for Roku API emulation.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/emulated_roku/
"""
import voluptuous as vol
from homeassistant import config_entries, util
from homeassistant.const import CONF_NAME
import homeassistant.helpers.config_validation as cv
from .binding import EmulatedRoku
from .config_flow import configured_servers
from .const import (
CONF_ADVERTISE_IP, CONF_ADVERTISE_PORT, CONF_HOST_IP, CONF_LISTEN_PORT,
CONF_SERVERS, CONF_UPNP_BIND_MULTICAST, DOMAIN)
REQUIREMENTS = ['emulated_roku==0.1.7']
SERVER_CONFIG_SCHEMA = vol.Schema({
vol.Required(CONF_NAME): cv.string,
vol.Required(CONF_LISTEN_PORT): cv.port,
vol.Optional(CONF_HOST_IP): cv.string,
vol.Optional(CONF_ADVERTISE_IP): cv.string,
vol.Optional(CONF_ADVERTISE_PORT): cv.port,
vol.Optional(CONF_UPNP_BIND_MULTICAST): cv.boolean
})
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_SERVERS):
vol.All(cv.ensure_list, [SERVER_CONFIG_SCHEMA]),
}),
}, extra=vol.ALLOW_EXTRA)
async def async_setup(hass, config):
"""Set up the emulated roku component."""
conf = config.get(DOMAIN)
if conf is None:
return True
existing_servers = configured_servers(hass)
for entry in conf[CONF_SERVERS]:
if entry[CONF_NAME] not in existing_servers:
hass.async_create_task(hass.config_entries.flow.async_init(
DOMAIN,
context={'source': config_entries.SOURCE_IMPORT},
data=entry
))
return True
async def async_setup_entry(hass, config_entry):
"""Set up an emulated roku server from a config entry."""
config = config_entry.data
if DOMAIN not in hass.data:
hass.data[DOMAIN] = {}
name = config[CONF_NAME]
listen_port = config[CONF_LISTEN_PORT]
host_ip = config.get(CONF_HOST_IP) or util.get_local_ip()
advertise_ip = config.get(CONF_ADVERTISE_IP)
advertise_port = config.get(CONF_ADVERTISE_PORT)
upnp_bind_multicast = config.get(CONF_UPNP_BIND_MULTICAST)
server = EmulatedRoku(hass, name, host_ip, listen_port,
advertise_ip, advertise_port, upnp_bind_multicast)
hass.data[DOMAIN][name] = server
return await server.setup()
async def async_unload_entry(hass, entry):
"""Unload a config entry."""
name = entry.data[CONF_NAME]
server = hass.data[DOMAIN].pop(name)
return await server.unload()

View File

@ -0,0 +1,147 @@
"""Bridge between emulated_roku and Home Assistant."""
import logging
from homeassistant.const import (
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
from homeassistant.core import CoreState, EventOrigin
LOGGER = logging.getLogger('homeassistant.components.emulated_roku')
EVENT_ROKU_COMMAND = 'roku_command'
ATTR_COMMAND_TYPE = 'type'
ATTR_SOURCE_NAME = 'source_name'
ATTR_KEY = 'key'
ATTR_APP_ID = 'app_id'
ROKU_COMMAND_KEYDOWN = 'keydown'
ROKU_COMMAND_KEYUP = 'keyup'
ROKU_COMMAND_KEYPRESS = 'keypress'
ROKU_COMMAND_LAUNCH = 'launch'
class EmulatedRoku:
"""Manages an emulated_roku server."""
def __init__(self, hass, name, host_ip, listen_port,
advertise_ip, advertise_port, upnp_bind_multicast):
"""Initialize the properties."""
self.hass = hass
self.roku_usn = name
self.host_ip = host_ip
self.listen_port = listen_port
self.advertise_port = advertise_port
self.advertise_ip = advertise_ip
self.bind_multicast = upnp_bind_multicast
self._api_server = None
self._unsub_start_listener = None
self._unsub_stop_listener = None
async def setup(self):
"""Start the emulated_roku server."""
from emulated_roku import EmulatedRokuServer, \
EmulatedRokuCommandHandler
class EventCommandHandler(EmulatedRokuCommandHandler):
"""emulated_roku command handler to turn commands into events."""
def __init__(self, hass):
self.hass = hass
def on_keydown(self, roku_usn, key):
"""Handle keydown event."""
self.hass.bus.async_fire(EVENT_ROKU_COMMAND, {
ATTR_SOURCE_NAME: roku_usn,
ATTR_COMMAND_TYPE: ROKU_COMMAND_KEYDOWN,
ATTR_KEY: key
}, EventOrigin.local)
def on_keyup(self, roku_usn, key):
"""Handle keyup event."""
self.hass.bus.async_fire(EVENT_ROKU_COMMAND, {
ATTR_SOURCE_NAME: roku_usn,
ATTR_COMMAND_TYPE: ROKU_COMMAND_KEYUP,
ATTR_KEY: key
}, EventOrigin.local)
def on_keypress(self, roku_usn, key):
"""Handle keypress event."""
self.hass.bus.async_fire(EVENT_ROKU_COMMAND, {
ATTR_SOURCE_NAME: roku_usn,
ATTR_COMMAND_TYPE: ROKU_COMMAND_KEYPRESS,
ATTR_KEY: key
}, EventOrigin.local)
def launch(self, roku_usn, app_id):
"""Handle launch event."""
self.hass.bus.async_fire(EVENT_ROKU_COMMAND, {
ATTR_SOURCE_NAME: roku_usn,
ATTR_COMMAND_TYPE: ROKU_COMMAND_LAUNCH,
ATTR_APP_ID: app_id
}, EventOrigin.local)
LOGGER.debug("Intializing emulated_roku %s on %s:%s",
self.roku_usn, self.host_ip, self.listen_port)
handler = EventCommandHandler(self.hass)
self._api_server = EmulatedRokuServer(
self.hass.loop, handler,
self.roku_usn, self.host_ip, self.listen_port,
advertise_ip=self.advertise_ip,
advertise_port=self.advertise_port,
bind_multicast=self.bind_multicast
)
async def emulated_roku_stop(event):
"""Wrap the call to emulated_roku.close."""
LOGGER.debug("Stopping emulated_roku %s", self.roku_usn)
self._unsub_stop_listener = None
await self._api_server.close()
async def emulated_roku_start(event):
"""Wrap the call to emulated_roku.start."""
try:
LOGGER.debug("Starting emulated_roku %s", self.roku_usn)
self._unsub_start_listener = None
await self._api_server.start()
except OSError:
LOGGER.exception("Failed to start Emulated Roku %s on %s:%s",
self.roku_usn, self.host_ip, self.listen_port)
# clean up inconsistent state on errors
await emulated_roku_stop(None)
else:
self._unsub_stop_listener = self.hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STOP,
emulated_roku_stop)
# start immediately if already running
if self.hass.state == CoreState.running:
await emulated_roku_start(None)
else:
self._unsub_start_listener = self.hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_START,
emulated_roku_start)
return True
async def unload(self):
"""Unload the emulated_roku server."""
LOGGER.debug("Unloading emulated_roku %s", self.roku_usn)
if self._unsub_start_listener:
self._unsub_start_listener()
self._unsub_start_listener = None
if self._unsub_stop_listener:
self._unsub_stop_listener()
self._unsub_stop_listener = None
await self._api_server.close()
return True

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