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/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/fibaro.py
homeassistant/components/fibaro/__init__.py
homeassistant/components/*/fibaro.py
homeassistant/components/gc100.py
@ -234,7 +239,7 @@ omit =
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
@ -276,7 +281,8 @@ omit =
homeassistant/components/*/opentherm_gw.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
@ -298,7 +304,9 @@ omit =
homeassistant/components/*/raincloud.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
@ -308,6 +316,9 @@ omit =
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
@ -327,7 +338,7 @@ omit =
homeassistant/components/*/sense.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
@ -424,8 +435,14 @@ omit =
homeassistant/components/*/zabbix.py
homeassistant/components/zha/__init__.py
homeassistant/components/zha/binary_sensor.py
homeassistant/components/zha/const.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/helpers.py
homeassistant/components/*/zha.py
@ -519,7 +536,6 @@ omit =
homeassistant/components/device_tracker/fritz.py
homeassistant/components/device_tracker/google_maps.py
homeassistant/components/device_tracker/googlehome.py
homeassistant/components/device_tracker/gpslogger.py
homeassistant/components/device_tracker/hitron_coda.py
homeassistant/components/device_tracker/huawei_router.py
homeassistant/components/device_tracker/icloud.py
@ -637,7 +653,6 @@ omit =
homeassistant/components/media_player/pioneer.py
homeassistant/components/media_player/pjlink.py
homeassistant/components/media_player/plex.py
homeassistant/components/media_player/roku.py
homeassistant/components/media_player/russound_rio.py
homeassistant/components/media_player/russound_rnet.py
homeassistant/components/media_player/snapcast.py

1
.gitignore vendored
View File

@ -78,6 +78,7 @@ venv
.venv
Pipfile*
share/*
Scripts/
# vimmy stuff
*.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/esphome/*.py @OttoWinter
homeassistant/components/*/esphome.py @OttoWinter
# H
homeassistant/components/hive.py @Rendili @KJonline
@ -219,7 +218,6 @@ homeassistant/components/*/ness_alarm.py @nickw444
# O
homeassistant/components/openuv/* @bachya
homeassistant/components/*/openuv.py @bachya
# P
homeassistant/components/point/* @fredrike
@ -231,13 +229,11 @@ homeassistant/components/*/qwikswitch.py @kellerza
# R
homeassistant/components/rainmachine/* @bachya
homeassistant/components/*/rainmachine.py @bachya
homeassistant/components/*/random.py @fabaff
homeassistant/components/*/rfxtrx.py @danielhiversen
# S
homeassistant/components/simplisafe/* @bachya
homeassistant/components/*/simplisafe.py @bachya
# T
homeassistant/components/tahoma.py @philklei

View File

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

View File

@ -1,6 +1,7 @@
"""Home Assistant auth provider."""
import base64
from collections import OrderedDict
import logging
from typing import Any, Dict, List, Optional, cast
import bcrypt
@ -51,6 +52,15 @@ class Data:
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY,
private=True)
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:
"""Load stored data."""
@ -61,6 +71,20 @@ class Data:
'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
@property
@ -73,6 +97,7 @@ class Data:
Raises InvalidAuth if auth invalid.
"""
username = self.normalize_username(username)
dummy = b'$2b$12$CiuFGszHx9eNHxPuQcwBWez4CwDTOcLTX5CbOpV6gef2nYuXkY7BO'
found = None
@ -105,7 +130,10 @@ class Data:
def add_auth(self, username: str, password: str) -> None:
"""Add a new authenticated user/pass."""
if any(user['username'] == username for user in self.users):
username = self.normalize_username(username)
if any(self.normalize_username(user['username']) == username
for user in self.users):
raise InvalidUser
self.users.append({
@ -116,9 +144,11 @@ class Data:
@callback
def async_remove_auth(self, username: str) -> None:
"""Remove authentication."""
username = self.normalize_username(username)
index = None
for i, user in enumerate(self.users):
if user['username'] == username:
if self.normalize_username(user['username']) == username:
index = i
break
@ -132,8 +162,10 @@ class Data:
Raises InvalidUser if user cannot be found.
"""
username = self.normalize_username(username)
for user in self.users:
if user['username'] == username:
if self.normalize_username(user['username']) == username:
user['password'] = self.hash_password(
new_password, True).decode()
break
@ -178,10 +210,15 @@ class HassAuthProvider(AuthProvider):
async def async_get_or_create_credentials(
self, flow_result: Dict[str, str]) -> Credentials:
"""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():
if credential.data['username'] == username:
if norm_username(credential.data['username']) == username:
return credential
# 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.entity import Entity
REQUIREMENTS = ['abodepy==0.14.0']
REQUIREMENTS = ['abodepy==0.15.0']
_LOGGER = logging.getLogger(__name__)

View File

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

View File

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

View File

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

View File

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

View File

@ -17,7 +17,7 @@ from homeassistant.components.arlo import (
DATA_ARLO, CONF_ATTRIBUTION, SIGNAL_UPDATE_ARLO)
from homeassistant.const import (
ATTR_ATTRIBUTION, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
STATE_ALARM_DISARMED)
STATE_ALARM_DISARMED, STATE_ALARM_ARMED_NIGHT)
_LOGGER = logging.getLogger(__name__)
@ -25,6 +25,7 @@ ARMED = 'armed'
CONF_HOME_MODE_NAME = 'home_mode_name'
CONF_AWAY_MODE_NAME = 'away_mode_name'
CONF_NIGHT_MODE_NAME = 'night_mode_name'
DEPENDENCIES = ['arlo']
@ -35,6 +36,7 @@ ICON = 'mdi:security'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_HOME_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)
away_mode_name = config.get(CONF_AWAY_MODE_NAME)
night_mode_name = config.get(CONF_NIGHT_MODE_NAME)
base_stations = []
for base_station in arlo.base_stations:
base_stations.append(ArloBaseStation(base_station, home_mode_name,
away_mode_name))
away_mode_name, night_mode_name))
add_entities(base_stations, True)
class ArloBaseStation(AlarmControlPanel):
"""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."""
self._base_station = data
self._home_mode_name = home_mode_name
self._away_mode_name = away_mode_name
self._night_mode_name = night_mode_name
self._state = None
@property
@ -105,6 +109,10 @@ class ArloBaseStation(AlarmControlPanel):
"""Send arm home command. Uses custom mode."""
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
def name(self):
"""Return the name of the base station."""
@ -128,4 +136,6 @@ class ArloBaseStation(AlarmControlPanel):
return STATE_ALARM_ARMED_HOME
if mode == self._away_mode_name:
return STATE_ALARM_ARMED_AWAY
if mode == self._night_mode_name:
return STATE_ALARM_ARMED_NIGHT
return mode

View File

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

View File

@ -116,7 +116,7 @@ class ElkArea(ElkEntity, alarm.AlarmControlPanel):
@property
def code_format(self):
"""Return the alarm code format."""
return '^[0-9]{4}([0-9]{2})?$'
return alarm.FORMAT_NUMBER
@property
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."""
if self._code:
return None
return 'Number'
return alarm.FORMAT_NUMBER
@property
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:
return None
if isinstance(self._code, str) and re.search('^\\d+$', self._code):
return 'Number'
return 'Any'
return alarm.FORMAT_NUMBER
return alarm.FORMAT_TEXT
@property
def state(self):

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -72,6 +72,6 @@ async def async_setup(hass, config):
pass
else:
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

View File

@ -27,8 +27,9 @@ from homeassistant.const import (
CONF_NAME, SERVICE_LOCK, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE,
SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP,
SERVICE_SET_COVER_POSITION, SERVICE_TURN_OFF, SERVICE_TURN_ON,
SERVICE_UNLOCK, SERVICE_VOLUME_SET, STATE_LOCKED, STATE_ON, STATE_UNLOCKED,
TEMP_CELSIUS, TEMP_FAHRENHEIT, MATCH_ALL)
SERVICE_UNLOCK, SERVICE_VOLUME_SET, STATE_LOCKED, STATE_ON,
STATE_UNAVAILABLE, STATE_UNLOCKED, TEMP_CELSIUS, TEMP_FAHRENHEIT,
MATCH_ALL)
import homeassistant.core as ha
import homeassistant.util.color as color_util
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):
"""Implements Alexa.PowerController.
@ -769,7 +801,8 @@ class _GenericCapabilities(_AlexaEntity):
return [_DisplayCategory.OTHER]
def interfaces(self):
return [_AlexaPowerController(self.entity)]
return [_AlexaPowerController(self.entity),
_AlexaEndpointHealth(self.hass, self.entity)]
@ENTITY_ADAPTERS.register(switch.DOMAIN)
@ -778,7 +811,8 @@ class _SwitchCapabilities(_AlexaEntity):
return [_DisplayCategory.SWITCH]
def interfaces(self):
return [_AlexaPowerController(self.entity)]
return [_AlexaPowerController(self.entity),
_AlexaEndpointHealth(self.hass, self.entity)]
@ENTITY_ADAPTERS.register(climate.DOMAIN)
@ -792,6 +826,7 @@ class _ClimateCapabilities(_AlexaEntity):
yield _AlexaPowerController(self.entity)
yield _AlexaThermostatController(self.hass, self.entity)
yield _AlexaTemperatureSensor(self.hass, self.entity)
yield _AlexaEndpointHealth(self.hass, self.entity)
@ENTITY_ADAPTERS.register(cover.DOMAIN)
@ -804,6 +839,7 @@ class _CoverCapabilities(_AlexaEntity):
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if supported & cover.SUPPORT_SET_POSITION:
yield _AlexaPercentageController(self.entity)
yield _AlexaEndpointHealth(self.hass, self.entity)
@ENTITY_ADAPTERS.register(light.DOMAIN)
@ -821,6 +857,7 @@ class _LightCapabilities(_AlexaEntity):
yield _AlexaColorController(self.entity)
if supported & light.SUPPORT_COLOR_TEMP:
yield _AlexaColorTemperatureController(self.entity)
yield _AlexaEndpointHealth(self.hass, self.entity)
@ENTITY_ADAPTERS.register(fan.DOMAIN)
@ -833,6 +870,7 @@ class _FanCapabilities(_AlexaEntity):
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if supported & fan.SUPPORT_SET_SPEED:
yield _AlexaPercentageController(self.entity)
yield _AlexaEndpointHealth(self.hass, self.entity)
@ENTITY_ADAPTERS.register(lock.DOMAIN)
@ -841,7 +879,8 @@ class _LockCapabilities(_AlexaEntity):
return [_DisplayCategory.SMARTLOCK]
def interfaces(self):
return [_AlexaLockController(self.entity)]
return [_AlexaLockController(self.entity),
_AlexaEndpointHealth(self.hass, self.entity)]
@ENTITY_ADAPTERS.register(media_player.DOMAIN)
@ -851,6 +890,7 @@ class _MediaPlayerCapabilities(_AlexaEntity):
def interfaces(self):
yield _AlexaPowerController(self.entity)
yield _AlexaEndpointHealth(self.hass, self.entity)
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
if supported & media_player.SUPPORT_VOLUME_SET:
@ -913,6 +953,7 @@ class _SensorCapabilities(_AlexaEntity):
TEMP_CELSIUS,
):
yield _AlexaTemperatureSensor(self.hass, self.entity)
yield _AlexaEndpointHealth(self.hass, self.entity)
@ENTITY_ADAPTERS.register(binary_sensor.DOMAIN)
@ -934,6 +975,8 @@ class _BinarySensorCapabilities(_AlexaEntity):
elif sensor_type is self.TYPE_MOTION:
yield _AlexaMotionSensor(self.hass, self.entity)
yield _AlexaEndpointHealth(self.hass, self.entity)
def get_type(self):
"""Return the type of binary sensor."""
attrs = self.entity.attributes
@ -993,8 +1036,7 @@ class Config:
self.entity_config = entity_config or {}
@ha.callback
def async_setup(hass, config):
async def async_setup(hass, config):
"""Activate Smart Home functionality of Alexa component.
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))
if AUTH_KEY in hass.data:
hass.loop.create_task(
async_enable_proactive_mode(hass, smart_home_config))
await 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
headers = {
"Authorization": "Bearer {}".format(token),
"Content-Type": "application/json;charset=UTF-8"
"Authorization": "Bearer {}".format(token)
}
endpoint = alexa_entity.entity_id()
@ -1359,14 +1399,14 @@ async def async_send_changereport_message(hass, config, alexa_entity):
payload=payload)
message.set_endpoint_full(token, endpoint)
message_str = json.dumps(message.serialize())
message_serialized = message.serialize()
try:
session = aiohttp_client.async_get_clientsession(hass)
with async_timeout.timeout(DEFAULT_TIMEOUT, loop=hass.loop):
response = await session.post(config.endpoint,
headers=headers,
data=message_str,
json=message_serialized,
allow_redirects=True)
except (asyncio.TimeoutError, aiohttp.ClientError):
@ -1375,7 +1415,7 @@ async def async_send_changereport_message(hass, config, alexa_entity):
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)
if response.status != 202:

View File

@ -26,6 +26,8 @@ CONF_SSH_KEY = 'ssh_key'
CONF_REQUIRE_IP = 'require_ip'
DEFAULT_SSH_PORT = 22
SECRET_GROUP = 'Password or SSH Key'
CONF_SENSORS = 'sensors'
SENSOR_TYPES = ['upload_speed', 'download_speed', 'download', 'upload']
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
@ -37,7 +39,9 @@ CONFIG_SCHEMA = vol.Schema({
vol.Optional(CONF_REQUIRE_IP, default=True): cv.boolean,
vol.Exclusive(CONF_PASSWORD, SECRET_GROUP): cv.string,
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)
@ -62,7 +66,8 @@ async def async_setup(hass, config):
hass.data[DATA_ASUSWRT] = api
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, 'device_tracker', DOMAIN, {}, config))
return True

View File

@ -9,11 +9,11 @@
},
"step": {
"init": {
"description": "Bitte w\u00e4hle einen der Benachrichtigungsdienste:",
"description": "Bitte w\u00e4hlen Sie einen der Benachrichtigungsdienste:",
"title": "Einmal Passwort f\u00fcr Notify einrichten"
},
"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"
}
},

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):
"""Execute an action."""
_LOGGER.info('Executing %s', name)
hass.components.logbook.async_log_entry(
name, 'has been triggered', DOMAIN, entity_id)
try:
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
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

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.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({
TRIGGER_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): 'time',
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))
vol.Required(CONF_AT): cv.time,
})
async def async_trigger(hass, config, action, automation_info):
"""Listen for state changes based on configuration."""
if CONF_AT in config:
at_time = config.get(CONF_AT)
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)
at_time = config.get(CONF_AT)
hours, minutes, seconds = at_time.hour, at_time.minute, at_time.second
@callback
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({
DOMAIN: vol.Schema({
cv.slug: DEVICE_SCHEMA,
}),
DOMAIN: cv.schema_with_slug_keys(DEVICE_SCHEMA),
}, extra=vol.ALLOW_EXTRA)
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 (
BinarySensorDevice, ENTITY_ID_FORMAT)
from homeassistant.components.fibaro import (
FIBARO_CONTROLLER, FIBARO_DEVICES, FibaroDevice)
FIBARO_DEVICES, FibaroDevice)
from homeassistant.const import (CONF_DEVICE_CLASS, CONF_ICON)
DEPENDENCIES = ['fibaro']
@ -33,17 +33,17 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
return
add_entities(
[FibaroBinarySensor(device, hass.data[FIBARO_CONTROLLER])
[FibaroBinarySensor(device)
for device in hass.data[FIBARO_DEVICES]['binary_sensor']], True)
class FibaroBinarySensor(FibaroDevice, BinarySensorDevice):
"""Representation of a Fibaro Binary Sensor."""
def __init__(self, fibaro_device, controller):
def __init__(self, fibaro_device):
"""Initialize the binary_sensor."""
self._state = None
super().__init__(fibaro_device, controller)
super().__init__(fibaro_device)
self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id)
stype = None
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/
"""
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.hive import DATA_HIVE
from homeassistant.components.hive import DATA_HIVE, DOMAIN
DEPENDENCIES = ['hive']
@ -35,9 +35,24 @@ class HiveBinarySensorEntity(BinarySensorDevice):
self.attributes = {}
self.data_updatesource = '{}.{}'.format(self.device_type,
self.node_id)
self._unique_id = '{}-{}'.format(self.node_id, self.device_type)
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):
"""Handle the new update request."""
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({
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({
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
import homeassistant.helpers.config_validation as cv
REQUIREMENTS = ['holidays==0.9.8']
REQUIREMENTS = ['holidays==0.9.9']
_LOGGER = logging.getLogger(__name__)
@ -26,6 +26,7 @@ ALL_COUNTRIES = [
'Canada', 'CA', 'Colombia', 'CO', 'Croatia', 'HR', 'Czech', 'CZ',
'Denmark', 'DK', 'England', 'EuropeanCentralBank', 'ECB', 'TAR',
'Finland', 'FI', 'France', 'FRA', 'Germany', 'DE', 'Hungary', 'HU',
'Honduras', 'HUD',
'India', 'IND', 'Ireland', 'Isle of Man', 'Italy', 'IT', 'Japan', 'JP',
'Mexico', 'MX', 'Netherlands', 'NL', 'NewZealand', 'NZ',
'Northern Ireland', 'Norway', 'NO', 'Polish', 'PL', 'Portugal', 'PT',

View File

@ -107,7 +107,7 @@ class XiaomiBinarySensor(XiaomiDevice, BinarySensorDevice):
def update(self):
"""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)
@ -178,7 +178,28 @@ class XiaomiMotionSensor(XiaomiBinarySensor):
self.async_schedule_update_ha_state()
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':
_LOGGER.debug(
'Skipping heartbeat of the motion sensor. '
@ -187,8 +208,7 @@ class XiaomiMotionSensor(XiaomiBinarySensor):
'11631#issuecomment-357507744).')
return
self._should_poll = False
if NO_MOTION in data: # handle push from the hub
if NO_MOTION in data:
self._no_motion_since = data[NO_MOTION]
self._state = False
return True
@ -203,26 +223,20 @@ class XiaomiMotionSensor(XiaomiBinarySensor):
self._unsub_set_no_motion()
self._unsub_set_no_motion = async_call_later(
self._hass,
180,
120,
self._async_set_no_motion
)
else:
self._should_poll = True
if self.entity_id is not None:
self._hass.bus.fire('xiaomi_aqara.motion', {
'entity_id': self.entity_id
})
if self.entity_id is not None:
self._hass.bus.fire('xiaomi_aqara.motion', {
'entity_id': self.entity_id
})
self._no_motion_since = 0
if self._state:
return False
self._state = True
return True
if value == NO_MOTION:
if not self._state:
return False
self._state = False
return True
class XiaomiDoorSensor(XiaomiBinarySensor):

View File

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

View File

@ -7,6 +7,7 @@ https://www.home-assistant.io/components/camera.proxy/
import asyncio
import logging
from datetime import timedelta
import voluptuous as vol
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
from homeassistant.components.camera import async_get_still_stream
REQUIREMENTS = ['pillow==5.3.0']
REQUIREMENTS = ['pillow==5.4.1']
_LOGGER = logging.getLogger(__name__)
@ -206,7 +207,7 @@ class ProxyCamera(Camera):
self._cache_images = bool(
config.get(CONF_IMAGE_REFRESH_RATE)
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._headers = (
{HTTP_HEADER_HA_AUTH: self.hass.config.api.api_password}
@ -223,7 +224,8 @@ class ProxyCamera(Camera):
now = dt_util.utcnow()
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
self._last_image_time = now

View File

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

View File

@ -19,17 +19,18 @@ DEPENDENCIES = ['zoneminder']
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the ZoneMinder cameras."""
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 = []
for monitor in monitors:
_LOGGER.info("Initializing camera %s", monitor.id)
cameras.append(ZoneMinderCamera(monitor, zm_client.verify_ssl))
for zm_client in hass.data[ZONEMINDER_DOMAIN].values():
monitors = zm_client.get_monitors()
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)

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,
SUPPORT_AUX_HEAT, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE)
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']
HIVE_TO_HASS_STATE = {'SCHEDULE': STATE_AUTO, 'MANUAL': STATE_HEAT,
@ -44,6 +44,7 @@ class HiveClimateEntity(ClimateDevice):
self.attributes = {}
self.data_updatesource = '{}.{}'.format(self.device_type,
self.node_id)
self._unique_id = '{}-{}'.format(self.node_id, self.device_type)
if self.device_type == "Heating":
self.modes = [STATE_AUTO, STATE_HEAT, STATE_OFF]
@ -52,6 +53,21 @@ class HiveClimateEntity(ClimateDevice):
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
def supported_features(self):
"""Return the list of supported features."""

View File

@ -19,7 +19,8 @@ from homeassistant.components.climate import (
ATTR_CURRENT_HUMIDITY, ClimateDevice, DOMAIN, PLATFORM_SCHEMA,
SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_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.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
@ -57,6 +58,16 @@ FIELD_TO_FLAG = {
'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,
discovery_info=None):
@ -129,9 +140,10 @@ class SensiboClimate(ClimateDevice):
self._ac_states = data['acState']
self._status = data['connectionStatus']['isAlive']
capabilities = data['remoteCapabilities']
self._operations = sorted(capabilities['modes'].keys())
self._current_capabilities = capabilities[
'modes'][self.current_operation]
self._operations = [SENSIBO_TO_HA[mode] for mode
in capabilities['modes']]
self._current_capabilities = \
capabilities['modes'][self._ac_states['mode']]
temperature_unit_key = data.get('temperatureUnit') or \
self._ac_states.get('temperatureUnit')
if temperature_unit_key:
@ -186,7 +198,7 @@ class SensiboClimate(ClimateDevice):
@property
def current_operation(self):
"""Return current operation ie. heat, cool, idle."""
return self._ac_states['mode']
return SENSIBO_TO_HA.get(self._ac_states['mode'])
@property
def current_humidity(self):
@ -293,7 +305,8 @@ class SensiboClimate(ClimateDevice):
"""Set new target operation mode."""
with async_timeout.timeout(TIMEOUT):
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):
"""Set new target swing operation."""

View File

@ -2,6 +2,7 @@
import asyncio
import logging
import pprint
import random
import uuid
from aiohttp import hdrs, client_exceptions, WSMsgType
@ -107,9 +108,11 @@ class CloudIoT:
self.tries += 1
try:
# Sleep 2^tries seconds between retries
self.retry_task = hass.async_create_task(asyncio.sleep(
2**min(9, self.tries), loop=hass.loop))
# Sleep 2^tries + 0…tries*3 seconds between retries
self.retry_task = hass.async_create_task(
asyncio.sleep(2**min(9, self.tries) +
random.randint(0, self.tries * 3),
loop=hass.loop))
yield from self.retry_task
self.retry_task = None
except asyncio.CancelledError:
@ -313,15 +316,20 @@ def async_handle_google_actions(hass, cloud, payload):
@HANDLERS.register('cloud')
@asyncio.coroutine
def async_handle_cloud(hass, cloud, payload):
async def async_handle_cloud(hass, cloud, payload):
"""Handle an incoming IoT message for cloud component."""
action = payload['action']
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",
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:
_LOGGER.warning("Received unknown cloud action: %s", action)

View File

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

View File

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

View File

@ -47,7 +47,7 @@ COVER_SCHEMA = vol.Schema({
})
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({
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']
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({
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({
vol.Required(CONF_COVERS): vol.Schema({cv.slug: COVER_SCHEMA}),
vol.Required(CONF_COVERS): cv.schema_with_slug_keys(COVER_SCHEMA),
})
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": {
"data": {
"host": "Host",
"port": "Port (Standartwert : '80')"
"port": "Port"
},
"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": {
"data": {
"host": "Gostitelj",
"port": "Vrata (privzeta vrednost: '80')"
"port": "Vrata"
},
"title": "Dolo\u010dite deCONZ prehod"
},
@ -28,6 +28,6 @@
"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
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 (
ATTR_POSITION, CoverDevice, SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_STOP,
SUPPORT_SET_POSITION)
from homeassistant.core import callback
from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE
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']
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())
class DeconzCover(CoverDevice):
class DeconzCover(DeconzDevice, CoverDevice):
"""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."""
self._cover = cover
self.gateway = gateway
self.unsub_dispatcher = None
super().__init__(device, gateway)
self._features = SUPPORT_OPEN
self._features |= SUPPORT_CLOSE
self._features |= SUPPORT_STOP
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
def current_cover_position(self):
"""Return the current position of the cover."""
if self.is_closed:
return 0
return int(self._cover.brightness / 255 * 100)
return int(self._device.brightness / 255 * 100)
@property
def is_closed(self):
"""Return if the cover is closed."""
return not self._cover.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
return not self._device.state
@property
def device_class(self):
"""Return the class of the cover."""
if self._cover.type in DAMPERS:
if self._device.type in DAMPERS:
return 'damper'
if self._cover.type in WINDOW_COVERS:
if self._device.type in WINDOW_COVERS:
return 'window'
@property
@ -118,16 +86,6 @@ class DeconzCover(CoverDevice):
"""Flag supported 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):
"""Move the cover to a specific position."""
position = kwargs[ATTR_POSITION]
@ -135,7 +93,7 @@ class DeconzCover(CoverDevice):
if position > 0:
data['on'] = True
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):
"""Open cover."""
@ -150,25 +108,7 @@ class DeconzCover(CoverDevice):
async def async_stop_cover(self, **kwargs):
"""Stop cover."""
data = {'bri_inc': 0}
await self._cover.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),
}
await self._device.async_set_state(data)
class DeconzCoverZigbeeSpec(DeconzCover):
@ -177,12 +117,12 @@ class DeconzCoverZigbeeSpec(DeconzCover):
@property
def current_cover_position(self):
"""Return the current position of the cover."""
return 100 - int(self._cover.brightness / 255 * 100)
return 100 - int(self._device.brightness / 255 * 100)
@property
def is_closed(self):
"""Return if the cover is closed."""
return self._cover.state
return self._device.state
async def async_set_cover_position(self, **kwargs):
"""Move the cover to a specific position."""
@ -191,4 +131,4 @@ class DeconzCoverZigbeeSpec(DeconzCover):
if position < 100:
data['on'] = True
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
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 (
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_HS_COLOR,
ATTR_TRANSITION, EFFECT_COLORLOOP, FLASH_LONG, FLASH_SHORT,
SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT,
SUPPORT_FLASH, SUPPORT_TRANSITION, Light)
from homeassistant.core import callback
from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE
from homeassistant.helpers.dispatcher import async_dispatcher_connect
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']
@ -59,51 +60,30 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_add_group(gateway.api.groups.values())
class DeconzLight(Light):
class DeconzLight(DeconzDevice, 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."""
self._light = light
self.gateway = gateway
self.unsub_dispatcher = None
super().__init__(device, gateway)
self._features = SUPPORT_BRIGHTNESS
self._features |= SUPPORT_FLASH
self._features |= SUPPORT_TRANSITION
if self._light.ct is not None:
if self._device.ct is not None:
self._features |= SUPPORT_COLOR_TEMP
if self._light.xy is not None:
if self._device.xy is not None:
self._features |= SUPPORT_COLOR
if self._light.effect is not None:
if self._device.effect is not None:
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
def brightness(self):
"""Return the brightness of this light between 0..255."""
return self._light.brightness
return self._device.brightness
@property
def effect_list(self):
@ -113,48 +93,28 @@ class DeconzLight(Light):
@property
def color_temp(self):
"""Return the CT color value."""
if self._light.colormode != 'ct':
if self._device.colormode != 'ct':
return None
return self._light.ct
return self._device.ct
@property
def hs_color(self):
"""Return the hs color value."""
if self._light.colormode in ('xy', 'hs') and self._light.xy:
return color_util.color_xy_to_hs(*self._light.xy)
if self._device.colormode in ('xy', 'hs') and self._device.xy:
return color_util.color_xy_to_hs(*self._device.xy)
return None
@property
def is_on(self):
"""Return true if light is on."""
return self._light.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
return self._device.state
@property
def supported_features(self):
"""Flag supported 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):
"""Turn on light."""
data = {'on': True}
@ -185,7 +145,7 @@ class DeconzLight(Light):
else:
data['effect'] = 'none'
await self._light.async_set_state(data)
await self._device.async_set_state(data)
async def async_turn_off(self, **kwargs):
"""Turn off light."""
@ -203,31 +163,13 @@ class DeconzLight(Light):
data['alert'] = 'lselect'
del data['on']
await self._light.async_set_state(data)
await self._device.async_set_state(data)
@property
def device_state_attributes(self):
"""Return the device state attributes."""
attributes = {}
attributes['is_deconz_group'] = self._light.type == 'LightGroup'
if self._light.type == 'LightGroup':
attributes['all_on'] = self._light.all_on
attributes['is_deconz_group'] = self._device.type == 'LightGroup'
if self._device.type == 'LightGroup':
attributes['all_on'] = self._device.all_on
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__)
REQUIREMENTS = ['pygatt==3.2.0']
REQUIREMENTS = ['pygatt[GATTTOOL]==3.2.0']
BLE_PREFIX = 'BLE_'
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.util import slugify, dt as dt_util
REQUIREMENTS = ['locationsharinglib==3.0.9']
REQUIREMENTS = ['locationsharinglib==3.0.11']
_LOGGER = logging.getLogger(__name__)

View File

@ -89,5 +89,7 @@ class GoogleHomeDeviceScanner(DeviceScanner):
devices[uuid]['btle_mac_address'] = device['mac_address']
devices[uuid]['ghname'] = ghname
devices[uuid]['source_type'] = 'bluetooth'
if device['name']:
devices[uuid]['btle_name'] = device['name']
await self.scanner.clear_scan_result()
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/
"""
import logging
from hmac import compare_digest
from aiohttp.web import Request, HTTPUnauthorized
import voluptuous as vol
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.components.gpslogger import TRACKER_UPDATE
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.typing import HomeAssistantType, ConfigType
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['http']
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_PASSWORD): cv.string,
})
DEPENDENCIES = ['gpslogger']
async def async_setup_scanner(hass: HomeAssistantType, config: ConfigType,
async_see, discovery_info=None):
"""Set up an endpoint for the GPSLogger application."""
hass.http.register_view(GPSLoggerView(async_see, config))
return True
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(
"""Set up an endpoint for the GPSLogger device tracker."""
async def _set_location(device, gps_location, battery, accuracy, attrs):
"""Fire HA event to set location."""
await async_see(
dev_id=device,
gps=gps_location, battery=battery,
gps=gps_location,
battery=battery,
gps_accuracy=accuracy,
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
DEFAULT_TIMEOUT = 10
REQUIREMENTS = ['beautifulsoup4==4.6.3']
REQUIREMENTS = ['beautifulsoup4==4.7.1']
_LOGGER = logging.getLogger(__name__)
@ -81,13 +81,14 @@ class LinksysAPDeviceScanner(DeviceScanner):
request = self._make_request(interface)
self.last_results.extend(
[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')]
)
return True
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.
login = base64.b64encode(bytes(self.username, '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
https://home-assistant.io/components/device_tracker.locative/
"""
from functools import partial
import logging
from homeassistant.const import (
ATTR_LATITUDE, ATTR_LONGITUDE, STATE_NOT_HOME, HTTP_UNPROCESSABLE_ENTITY)
from homeassistant.components.http import HomeAssistantView
# pylint: disable=unused-import
from homeassistant.components.device_tracker import ( # NOQA
DOMAIN, PLATFORM_SCHEMA)
from homeassistant.components.locative import TRACKER_UPDATE
from homeassistant.helpers.dispatcher import async_dispatcher_connect
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['http']
URL = '/api/locative'
DEPENDENCIES = ['locative']
def setup_scanner(hass, config, see, discovery_info=None):
"""Set up an endpoint for the Locative application."""
hass.http.register_view(LocativeView(see))
async def async_setup_scanner(hass, config, async_see, discovery_info=None):
"""Set up an endpoint for the Locative device tracker."""
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
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."""
new_devices = mysensors.setup_mysensors_platform(
hass, DOMAIN, discovery_info, MySensorsDeviceScanner,
device_args=(async_see, ))
device_args=(hass, async_see))
if not new_devices:
return False
@ -37,12 +37,13 @@ async def async_setup_scanner(hass, config, async_see, discovery_info=None):
class MySensorsDeviceScanner(mysensors.device.MySensorsDevice):
"""Represent a MySensors scanner."""
def __init__(self, async_see, *args):
def __init__(self, hass, async_see, *args):
"""Set up instance."""
super().__init__(*args)
self.async_see = async_see
self.hass = hass
async def async_update_callback(self):
async def _async_update_callback(self):
"""Update the device."""
await self.async_update()
node = self.gateway.sensors[self.node_id]

View File

@ -14,7 +14,7 @@ from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
from homeassistant.const import CONF_HOST
REQUIREMENTS = ['pysnmp==4.4.6']
REQUIREMENTS = ['pysnmp==4.4.8']
_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
import homeassistant.util.dt as dt_util
REQUIREMENTS = ['pyunifi==2.13']
REQUIREMENTS = ['pyunifi==2.16']
_LOGGER = logging.getLogger(__name__)
CONF_PORT = 'port'

View File

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

View File

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

View File

@ -1,7 +1,15 @@
{
"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": {
"user": {
"description": "M\u00f6chten Sie Dialogflow wirklich 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.",
"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": {
"user": {
"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.",
"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": {
"user": {
"description": "Weet u zeker dat u Dialogflow wilt instellen?",

View File

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

View File

@ -6,15 +6,16 @@ https://home-assistant.io/components/doorbird/
"""
import logging
from urllib.error import HTTPError
import voluptuous as vol
from homeassistant.components.http import HomeAssistantView
from homeassistant.const import CONF_HOST, CONF_USERNAME, \
CONF_PASSWORD, CONF_NAME, CONF_DEVICES, CONF_MONITORED_CONDITIONS
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__)
@ -25,6 +26,7 @@ API_URL = '/api/{}'.format(DOMAIN)
CONF_CUSTOM_URL = 'hass_url_override'
CONF_DOORBELL_EVENTS = 'doorbell_events'
CONF_DOORBELL_NUMS = 'doorbell_numbers'
CONF_RELAY_NUMS = 'relay_numbers'
CONF_MOTION_EVENTS = 'motion_events'
CONF_TOKEN = 'token'
@ -37,6 +39,10 @@ SENSOR_TYPES = {
'name': 'Motion',
'device_class': 'motion',
},
'relay': {
'name': 'Relay',
'device_class': 'relay',
}
}
RESET_DEVICE_FAVORITES = 'doorbird_reset_favorites'
@ -47,6 +53,8 @@ DEVICE_SCHEMA = vol.Schema({
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_DOORBELL_NUMS, default=[1]): vol.All(
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_NAME): cv.string,
vol.Optional(CONF_MONITORED_CONDITIONS, default=[]):
@ -80,6 +88,7 @@ def setup(hass, config):
username = doorstation_config.get(CONF_USERNAME)
password = doorstation_config.get(CONF_PASSWORD)
doorbell_nums = doorstation_config.get(CONF_DOORBELL_NUMS)
relay_nums = doorstation_config.get(CONF_RELAY_NUMS)
custom_url = doorstation_config.get(CONF_CUSTOM_URL)
events = doorstation_config.get(CONF_MONITORED_CONDITIONS)
name = (doorstation_config.get(CONF_NAME)
@ -90,7 +99,7 @@ def setup(hass, config):
if status[0]:
doorstation = ConfiguredDoorBird(device, name, events, custom_url,
doorbell_nums, token)
doorbell_nums, relay_nums, token)
doorstations.append(doorstation)
_LOGGER.info('Connected to DoorBird "%s" as %s@%s',
doorstation.name, username, device_ip)
@ -105,7 +114,18 @@ def setup(hass, config):
# Subscribe to doorbell or motion 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
@ -148,13 +168,15 @@ def handle_event(event):
class ConfiguredDoorBird():
"""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."""
self._name = name
self._device = device
self._custom_url = custom_url
self._monitored_events = events
self._doorbell_nums = doorbell_nums
self._relay_nums = relay_nums
self._token = token
@property
@ -218,9 +240,9 @@ class ConfiguredDoorBird():
# Register HA URL as webhook if not already, then get the ID
if not self.webhook_is_registered(hass_url):
self.device.change_favorite('http',
'Home Assistant on {} ({} events)'
.format(hass_url, event), hass_url)
self.device.change_favorite('http', 'Home Assistant ({} events)'
.format(event), hass_url)
fav_id = self.get_webhook_id(hass_url)
if not fav_id:
@ -239,6 +261,11 @@ class ConfiguredDoorBird():
entry = self.device.get_schedule_entry(event, str(doorbell))
entry.output.append(output)
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:
entry = self.device.get_schedule_entry(event)
entry.output.append(output)
@ -303,6 +330,16 @@ class ConfiguredDoorBird():
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):
"""Provide a page for the device to call."""
@ -330,7 +367,14 @@ class DoorBirdRequestView(HomeAssistantView):
if request_token == '' or not authenticated:
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')

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