This commit is contained in:
kbickar
2018-10-23 15:24:16 +00:00
114 changed files with 2775 additions and 1220 deletions

View File

@@ -209,7 +209,6 @@ omit =
homeassistant/components/lutron_caseta.py
homeassistant/components/*/lutron_caseta.py
homeassistant/components/mailgun.py
homeassistant/components/*/mailgun.py
homeassistant/components/matrix.py
@@ -458,7 +457,6 @@ omit =
homeassistant/components/cover/myq.py
homeassistant/components/cover/opengarage.py
homeassistant/components/cover/rpi_gpio.py
homeassistant/components/cover/ryobi_gdo.py
homeassistant/components/cover/scsgate.py
homeassistant/components/device_tracker/actiontec.py
homeassistant/components/device_tracker/aruba.py
@@ -758,6 +756,7 @@ omit =
homeassistant/components/sensor/radarr.py
homeassistant/components/sensor/rainbird.py
homeassistant/components/sensor/ripple.py
homeassistant/components/sensor/rtorrent.py
homeassistant/components/sensor/scrape.py
homeassistant/components/sensor/sense.py
homeassistant/components/sensor/sensehat.py

View File

@@ -58,6 +58,7 @@ homeassistant/components/climate/mill.py @danielhiversen
homeassistant/components/climate/sensibo.py @andrey-git
homeassistant/components/cover/group.py @cdce8p
homeassistant/components/cover/template.py @PhracturedBlue
homeassistant/components/device_tracker/asuswrt.py @kennedyshead
homeassistant/components/device_tracker/automatic.py @armills
homeassistant/components/device_tracker/huawei_router.py @abmantis
homeassistant/components/device_tracker/quantum_gateway.py @cisasteelersfan

View File

@@ -16,7 +16,7 @@ from homeassistant.const import (
from homeassistant.helpers.event import track_time_interval
from homeassistant.helpers.dispatcher import dispatcher_send
REQUIREMENTS = ['pyarlo==0.2.0']
REQUIREMENTS = ['pyarlo==0.2.2']
_LOGGER = logging.getLogger(__name__)
@@ -81,7 +81,7 @@ def setup(hass, config):
def hub_refresh(event_time):
"""Call ArloHub to refresh information."""
_LOGGER.info("Updating Arlo Hub component")
_LOGGER.debug("Updating Arlo Hub component")
hass.data[DATA_ARLO].update(update_cameras=True,
update_base_station=True)
dispatcher_send(hass, SIGNAL_UPDATE_ARLO)

View File

@@ -225,8 +225,17 @@ class AugustData:
for doorbell in self._doorbells:
_LOGGER.debug("Updating status for %s",
doorbell.device_name)
detail_by_id[doorbell.device_id] = self._api.get_doorbell_detail(
self._access_token, doorbell.device_id)
try:
detail_by_id[doorbell.device_id] =\
self._api.get_doorbell_detail(
self._access_token, doorbell.device_id)
except RequestException as ex:
_LOGGER.error("Request error trying to retrieve doorbell"
" status for %s. %s", doorbell.device_name, ex)
detail_by_id[doorbell.device_id] = None
except Exception:
detail_by_id[doorbell.device_id] = None
raise
_LOGGER.debug("Completed retrieving doorbell details")
self._doorbell_detail_by_id = detail_by_id
@@ -260,8 +269,17 @@ class AugustData:
for lock in self._locks:
_LOGGER.debug("Updating status for %s",
lock.device_name)
state_by_id[lock.device_id] = self._api.get_lock_door_status(
self._access_token, lock.device_id)
try:
state_by_id[lock.device_id] = self._api.get_lock_door_status(
self._access_token, lock.device_id)
except RequestException as ex:
_LOGGER.error("Request error trying to retrieve door"
" status for %s. %s", lock.device_name, ex)
state_by_id[lock.device_id] = None
except Exception:
state_by_id[lock.device_id] = None
raise
_LOGGER.debug("Completed retrieving door status")
self._door_state_by_id = state_by_id
@@ -275,10 +293,27 @@ class AugustData:
for lock in self._locks:
_LOGGER.debug("Updating status for %s",
lock.device_name)
status_by_id[lock.device_id] = self._api.get_lock_status(
self._access_token, lock.device_id)
detail_by_id[lock.device_id] = self._api.get_lock_detail(
self._access_token, lock.device_id)
try:
status_by_id[lock.device_id] = self._api.get_lock_status(
self._access_token, lock.device_id)
except RequestException as ex:
_LOGGER.error("Request error trying to retrieve door"
" status for %s. %s", lock.device_name, ex)
status_by_id[lock.device_id] = None
except Exception:
status_by_id[lock.device_id] = None
raise
try:
detail_by_id[lock.device_id] = self._api.get_lock_detail(
self._access_token, lock.device_id)
except RequestException as ex:
_LOGGER.error("Request error trying to retrieve door"
" details for %s. %s", lock.device_name, ex)
detail_by_id[lock.device_id] = None
except Exception:
detail_by_id[lock.device_id] = None
raise
_LOGGER.debug("Completed retrieving locks status")
self._lock_status_by_id = status_by_id

View File

@@ -0,0 +1,34 @@
{
"mfa_setup": {
"notify": {
"abort": {
"no_available_service": "Nu sunt disponibile servicii de notificare."
},
"error": {
"invalid_code": "Cod invalid, va rugam incercati din nou."
},
"step": {
"init": {
"description": "Selecta\u021bi unul dintre serviciile de notificare:",
"title": "Configura\u021bi o parol\u0103 unic\u0103 livrat\u0103 de o component\u0103 de notificare"
},
"setup": {
"description": "O parol\u0103 unic\u0103 a fost trimis\u0103 prin **notify.{notify_service}**. Introduce\u021bi parola mai jos:",
"title": "Verifica\u021bi configurarea"
}
},
"title": "Notifica\u021bi o parol\u0103 unic\u0103"
},
"totp": {
"error": {
"invalid_code": "Cod invalid, va rugam incercati din nou. Dac\u0103 primi\u021bi aceast\u0103 eroare \u00een mod consecvent, asigura\u021bi-v\u0103 c\u0103 ceasul sistemului dvs. Home Assistant este corect."
},
"step": {
"init": {
"title": "Configura\u021bi autentificarea cu doi factori utiliz\u00e2nd TOTP"
}
},
"title": "TOTP"
}
}
}

View File

@@ -0,0 +1,74 @@
"""
Offer geo location 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
"""
import voluptuous as vol
from homeassistant.components.geo_location import DOMAIN
from homeassistant.core import callback
from homeassistant.const import (
CONF_EVENT, CONF_PLATFORM, CONF_SOURCE, CONF_ZONE, EVENT_STATE_CHANGED)
from homeassistant.helpers import (
condition, config_validation as cv)
from homeassistant.helpers.config_validation import entity_domain
EVENT_ENTER = 'enter'
EVENT_LEAVE = 'leave'
DEFAULT_EVENT = EVENT_ENTER
TRIGGER_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): 'geo_location',
vol.Required(CONF_SOURCE): cv.string,
vol.Required(CONF_ZONE): entity_domain('zone'),
vol.Required(CONF_EVENT, default=DEFAULT_EVENT):
vol.Any(EVENT_ENTER, EVENT_LEAVE),
})
def source_match(state, source):
"""Check if the state matches the provided source."""
return state and state.attributes.get('source') == source
async def async_trigger(hass, config, action):
"""Listen for state changes based on configuration."""
source = config.get(CONF_SOURCE).lower()
zone_entity_id = config.get(CONF_ZONE)
trigger_event = config.get(CONF_EVENT)
@callback
def state_change_listener(event):
"""Handle specific state changes."""
# Skip if the event is not a geo_location entity.
if not event.data.get('entity_id').startswith(DOMAIN):
return
# Skip if the event's source does not match the trigger's source.
from_state = event.data.get('old_state')
to_state = event.data.get('new_state')
if not source_match(from_state, source) \
and not source_match(to_state, source):
return
zone_state = hass.states.get(zone_entity_id)
from_match = condition.zone(hass, zone_state, from_state)
to_match = condition.zone(hass, zone_state, to_state)
# pylint: disable=too-many-boolean-expressions
if trigger_event == EVENT_ENTER and not from_match and to_match or \
trigger_event == EVENT_LEAVE and from_match and not to_match:
hass.async_run_job(action({
'trigger': {
'platform': 'geo_location',
'source': source,
'entity_id': event.data.get('entity_id'),
'from_state': from_state,
'to_state': to_state,
'zone': zone_state,
'event': trigger_event,
},
}, context=event.context))
return hass.bus.async_listen(EVENT_STATE_CHANGED, state_change_listener)

View File

@@ -19,14 +19,15 @@ SCAN_INTERVAL = timedelta(seconds=5)
def _retrieve_door_state(data, lock):
"""Get the latest state of the DoorSense sensor."""
from august.lock import LockDoorStatus
doorstate = data.get_door_state(lock.device_id)
return doorstate == LockDoorStatus.OPEN
return data.get_door_state(lock.device_id)
def _retrieve_online_state(data, doorbell):
"""Get the latest state of the sensor."""
detail = data.get_doorbell_detail(doorbell.device_id)
if detail is None:
return None
return detail.is_online
@@ -138,9 +139,10 @@ class AugustDoorBinarySensor(BinarySensorDevice):
"""Get the latest state of the sensor."""
state_provider = SENSOR_TYPES_DOOR[self._sensor_type][2]
self._state = state_provider(self._data, self._door)
self._available = self._state is not None
from august.lock import LockDoorStatus
self._available = self._state != LockDoorStatus.UNKNOWN
self._state = self._state == LockDoorStatus.OPEN
class AugustDoorbellBinarySensor(BinarySensorDevice):
@@ -152,6 +154,12 @@ class AugustDoorbellBinarySensor(BinarySensorDevice):
self._sensor_type = sensor_type
self._doorbell = doorbell
self._state = None
self._available = False
@property
def available(self):
"""Return the availability of this sensor."""
return self._available
@property
def is_on(self):
@@ -173,3 +181,4 @@ class AugustDoorbellBinarySensor(BinarySensorDevice):
"""Get the latest state of the sensor."""
state_provider = SENSOR_TYPES_DOORBELL[self._sensor_type][2]
self._state = state_provider(self._data, self._doorbell)
self._available = self._state is not None

View File

@@ -131,7 +131,14 @@ class MqttBinarySensor(MqttAvailability, MqttDiscoveryUpdate,
await MqttDiscoveryUpdate.async_added_to_hass(self)
@callback
def state_message_received(topic, payload, qos):
def off_delay_listener(now):
"""Switch device off after a delay."""
self._delay_listener = None
self._state = False
self.async_schedule_update_ha_state()
@callback
def state_message_received(_topic, payload, _qos):
"""Handle a new received MQTT state message."""
if self._template is not None:
payload = self._template.async_render_with_possible_json_value(
@@ -146,17 +153,10 @@ class MqttBinarySensor(MqttAvailability, MqttDiscoveryUpdate,
self._name, self._state_topic)
return
if self._delay_listener is not None:
self._delay_listener()
if (self._state and self._off_delay is not None):
@callback
def off_delay_listener(now):
"""Switch device off after a delay."""
self._delay_listener = None
self._state = False
self.async_schedule_update_ha_state()
if self._delay_listener is not None:
self._delay_listener()
self._delay_listener = evt.async_call_later(
self.hass, self._off_delay, off_delay_listener)

View File

@@ -14,15 +14,16 @@ 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.7']
REQUIREMENTS = ['holidays==0.9.8']
_LOGGER = logging.getLogger(__name__)
# List of all countries currently supported by holidays
# There seems to be no way to get the list out at runtime
ALL_COUNTRIES = [
'Argentina', 'AR', 'Australia', 'AU', 'Austria', 'AT', 'Belarus', 'BY'
'Belgium', 'BE', 'Canada', 'CA', 'Colombia', 'CO', 'Czech', 'CZ',
'Argentina', 'AR', 'Australia', 'AU', 'Austria', 'AT',
'Brazil', 'BR', 'Belarus', 'BY', 'Belgium', 'BE',
'Canada', 'CA', 'Colombia', 'CO', 'Croatia', 'HR', 'Czech', 'CZ',
'Denmark', 'DK', 'England', 'EuropeanCentralBank', 'ECB', 'TAR',
'Finland', 'FI', 'France', 'FRA', 'Germany', 'DE', 'Hungary', 'HU',
'India', 'IND', 'Ireland', 'Isle of Man', 'Italy', 'IT', 'Japan', 'JP',
@@ -30,7 +31,7 @@ ALL_COUNTRIES = [
'Northern Ireland', 'Norway', 'NO', 'Polish', 'PL', 'Portugal', 'PT',
'PortugalExt', 'PTE', 'Scotland', 'Slovenia', 'SI', 'Slovakia', 'SK',
'South Africa', 'ZA', 'Spain', 'ES', 'Sweden', 'SE', 'Switzerland', 'CH',
'UnitedKingdom', 'UK', 'UnitedStates', 'US', 'Wales',
'Ukraine', 'UA', 'UnitedKingdom', 'UK', 'UnitedStates', 'US', 'Wales',
]
ALLOWED_DAYS = WEEKDAYS + ['holiday']

View File

@@ -0,0 +1,15 @@
{
"config": {
"abort": {
"no_devices_found": "Nu s-au g\u0103sit dispozitive Google Cast \u00een re\u021bea.",
"single_instance_allowed": "Este necesar\u0103 o singur\u0103 configura\u021bie a serviciului Google Cast."
},
"step": {
"confirm": {
"description": "Dori\u021bi s\u0103 configura\u021bi Google Cast?",
"title": "Google Cast"
}
},
"title": "Google Cast"
}
}

View File

@@ -0,0 +1,176 @@
"""
Support for Dyson Pure Hot+Cool link fan.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/climate.dyson/
"""
import logging
from homeassistant.components.dyson import DYSON_DEVICES
from homeassistant.components.climate import (
ClimateDevice, STATE_HEAT, STATE_COOL, STATE_IDLE,
SUPPORT_TARGET_TEMPERATURE, SUPPORT_FAN_MODE, SUPPORT_OPERATION_MODE)
from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE
_LOGGER = logging.getLogger(__name__)
STATE_DIFFUSE = "Diffuse Mode"
STATE_FOCUS = "Focus Mode"
FAN_LIST = [STATE_FOCUS, STATE_DIFFUSE]
OPERATION_LIST = [STATE_HEAT, STATE_COOL]
SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE
| SUPPORT_OPERATION_MODE)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Dyson fan components."""
if discovery_info is None:
return
from libpurecoollink.dyson_pure_hotcool_link import DysonPureHotCoolLink
# Get Dyson Devices from parent component.
add_devices(
[DysonPureHotCoolLinkDevice(device)
for device in hass.data[DYSON_DEVICES]
if isinstance(device, DysonPureHotCoolLink)]
)
class DysonPureHotCoolLinkDevice(ClimateDevice):
"""Representation of a Dyson climate fan."""
def __init__(self, device):
"""Initialize the fan."""
self._device = device
self._current_temp = None
async def async_added_to_hass(self):
"""Call when entity is added to hass."""
self.hass.async_add_job(self._device.add_message_listener,
self.on_message)
def on_message(self, message):
"""Call when new messages received from the climate."""
from libpurecoollink.dyson_pure_state import DysonPureHotCoolState
if isinstance(message, DysonPureHotCoolState):
_LOGGER.debug("Message received for climate device %s : %s",
self.name, message)
self.schedule_update_ha_state()
@property
def should_poll(self):
"""No polling needed."""
return False
@property
def supported_features(self):
"""Return the list of supported features."""
return SUPPORT_FLAGS
@property
def name(self):
"""Return the display name of this climate."""
return self._device.name
@property
def temperature_unit(self):
"""Return the unit of measurement."""
return TEMP_CELSIUS
@property
def current_temperature(self):
"""Return the current temperature."""
if self._device.environmental_state:
temperature_kelvin = self._device.environmental_state.temperature
if temperature_kelvin != 0:
self._current_temp = float("{0:.1f}".format(
temperature_kelvin - 273))
return self._current_temp
@property
def target_temperature(self):
"""Return the target temperature."""
heat_target = int(self._device.state.heat_target) / 10
return int(heat_target - 273)
@property
def current_humidity(self):
"""Return the current humidity."""
if self._device.environmental_state:
if self._device.environmental_state.humidity == 0:
return None
return self._device.environmental_state.humidity
return None
@property
def current_operation(self):
"""Return current operation ie. heat, cool, idle."""
from libpurecoollink.const import HeatMode, HeatState
if self._device.state.heat_mode == HeatMode.HEAT_ON.value:
if self._device.state.heat_state == HeatState.HEAT_STATE_ON.value:
return STATE_HEAT
return STATE_IDLE
return STATE_COOL
@property
def operation_list(self):
"""Return the list of available operation modes."""
return OPERATION_LIST
@property
def current_fan_mode(self):
"""Return the fan setting."""
from libpurecoollink.const import FocusMode
if self._device.state.focus_mode == FocusMode.FOCUS_ON.value:
return STATE_FOCUS
return STATE_DIFFUSE
@property
def fan_list(self):
"""Return the list of available fan modes."""
return FAN_LIST
def set_temperature(self, **kwargs):
"""Set new target temperature."""
target_temp = kwargs.get(ATTR_TEMPERATURE)
if target_temp is None:
return
target_temp = int(target_temp)
_LOGGER.debug("Set %s temperature %s", self.name, target_temp)
# Limit the target temperature into acceptable range.
target_temp = min(self.max_temp, target_temp)
target_temp = max(self.min_temp, target_temp)
from libpurecoollink.const import HeatTarget, HeatMode
self._device.set_configuration(
heat_target=HeatTarget.celsius(target_temp),
heat_mode=HeatMode.HEAT_ON)
def set_fan_mode(self, fan_mode):
"""Set new fan mode."""
_LOGGER.debug("Set %s focus mode %s", self.name, fan_mode)
from libpurecoollink.const import FocusMode
if fan_mode == STATE_FOCUS:
self._device.set_configuration(focus_mode=FocusMode.FOCUS_ON)
elif fan_mode == STATE_DIFFUSE:
self._device.set_configuration(focus_mode=FocusMode.FOCUS_OFF)
def set_operation_mode(self, operation_mode):
"""Set operation mode."""
_LOGGER.debug("Set %s heat mode %s", self.name, operation_mode)
from libpurecoollink.const import HeatMode
if operation_mode == STATE_HEAT:
self._device.set_configuration(heat_mode=HeatMode.HEAT_ON)
elif operation_mode == STATE_COOL:
self._device.set_configuration(heat_mode=HeatMode.HEAT_OFF)
@property
def min_temp(self):
"""Return the minimum temperature."""
return 1
@property
def max_temp(self):
"""Return the maximum temperature."""
return 37

View File

@@ -380,6 +380,8 @@ class GenericThermostat(ClimateDevice):
async def async_turn_away_mode_on(self):
"""Turn away mode on by setting it on away hold indefinitely."""
if self._is_away:
return
self._is_away = True
self._saved_target_temp = self._target_temp
self._target_temp = self._away_temp
@@ -388,6 +390,8 @@ class GenericThermostat(ClimateDevice):
async def async_turn_away_mode_off(self):
"""Turn away off."""
if not self._is_away:
return
self._is_away = False
self._target_temp = self._saved_target_temp
await self._async_control_heating()

View File

@@ -17,7 +17,7 @@ from homeassistant.const import (
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
REQUIREMENTS = ['millheater==0.1.2']
REQUIREMENTS = ['millheater==0.2.0']
_LOGGER = logging.getLogger(__name__)
@@ -43,6 +43,7 @@ async def async_setup_platform(hass, config, async_add_entities,
_LOGGER.error("Failed to connect to Mill")
return
await mill_data_connection.update_rooms()
await mill_data_connection.update_heaters()
dev = []

View File

@@ -162,7 +162,7 @@ class Cloud:
@property
def subscription_expired(self):
"""Return a boolean if the subscription has expired."""
return dt_util.utcnow() > self.expiration_date + timedelta(days=3)
return dt_util.utcnow() > self.expiration_date + timedelta(days=7)
@property
def expiration_date(self):

View File

@@ -113,6 +113,24 @@ def check_token(cloud):
raise _map_aws_exception(err)
def renew_access_token(cloud):
"""Renew access token."""
from botocore.exceptions import ClientError
cognito = _cognito(
cloud,
access_token=cloud.access_token,
refresh_token=cloud.refresh_token)
try:
cognito.renew_access_token()
cloud.id_token = cognito.id_token
cloud.access_token = cognito.access_token
cloud.write_user_info()
except ClientError as err:
raise _map_aws_exception(err)
def _authenticate(cloud, email, password):
"""Log in and return an authenticated Cognito instance."""
from botocore.exceptions import ClientError

View File

@@ -14,7 +14,7 @@ from homeassistant.components import websocket_api
from . import auth_api
from .const import DOMAIN, REQUEST_TIMEOUT
from .iot import STATE_DISCONNECTED
from .iot import STATE_DISCONNECTED, STATE_CONNECTED
_LOGGER = logging.getLogger(__name__)
@@ -249,13 +249,28 @@ async def websocket_subscription(hass, connection, msg):
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
response = await cloud.fetch_subscription_info()
if response.status == 200:
connection.send_message(websocket_api.result_message(
msg['id'], await response.json()))
else:
if response.status != 200:
connection.send_message(websocket_api.error_message(
msg['id'], 'request_failed', 'Failed to request subscription'))
data = await response.json()
# Check if a user is subscribed but local info is outdated
# In that case, let's refresh and reconnect
if data.get('provider') and cloud.iot.state != STATE_CONNECTED:
_LOGGER.debug(
"Found disconnected account with valid subscriotion, connecting")
await hass.async_add_executor_job(
auth_api.renew_access_token, cloud)
# Cancel reconnect in progress
if cloud.iot.state != STATE_DISCONNECTED:
await cloud.iot.disconnect()
hass.async_create_task(cloud.iot.connect())
connection.send_message(websocket_api.result_message(msg['id'], data))
@websocket_api.async_response
async def websocket_update_prefs(hass, connection, msg):

View File

@@ -1,103 +0,0 @@
"""
Ryobi platform for the cover component.
For more details about this platform, please refer to the documentation
https://home-assistant.io/components/cover.ryobi_gdo/
"""
import logging
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.cover import (
CoverDevice, PLATFORM_SCHEMA, SUPPORT_OPEN, SUPPORT_CLOSE)
from homeassistant.const import (
CONF_USERNAME, CONF_PASSWORD, STATE_UNKNOWN, STATE_CLOSED)
REQUIREMENTS = ['py_ryobi_gdo==0.0.10']
_LOGGER = logging.getLogger(__name__)
CONF_DEVICE_ID = 'device_id'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]),
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_USERNAME): cv.string,
})
SUPPORTED_FEATURES = (SUPPORT_OPEN | SUPPORT_CLOSE)
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Ryobi covers."""
from py_ryobi_gdo import RyobiGDO as ryobi_door
covers = []
username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
devices = config.get(CONF_DEVICE_ID)
for device_id in devices:
my_door = ryobi_door(username, password, device_id)
_LOGGER.debug("Getting the API key")
if my_door.get_api_key() is False:
_LOGGER.error("Wrong credentials, no API key retrieved")
return
_LOGGER.debug("Checking if the device ID is present")
if my_door.check_device_id() is False:
_LOGGER.error("%s not in your device list", device_id)
return
_LOGGER.debug("Adding device %s to covers", device_id)
covers.append(RyobiCover(hass, my_door))
if covers:
_LOGGER.debug("Adding covers")
add_entities(covers, True)
class RyobiCover(CoverDevice):
"""Representation of a ryobi cover."""
def __init__(self, hass, ryobi_door):
"""Initialize the cover."""
self.ryobi_door = ryobi_door
self._name = 'ryobi_gdo_{}'.format(ryobi_door.get_device_id())
self._door_state = None
@property
def name(self):
"""Return the name of the cover."""
return self._name
@property
def is_closed(self):
"""Return if the cover is closed."""
if self._door_state == STATE_UNKNOWN:
return False
return self._door_state == STATE_CLOSED
@property
def device_class(self):
"""Return the class of this device, from component DEVICE_CLASSES."""
return 'garage'
@property
def supported_features(self):
"""Flag supported features."""
return SUPPORTED_FEATURES
def close_cover(self, **kwargs):
"""Close the cover."""
_LOGGER.debug("Closing garage door")
self.ryobi_door.close_device()
def open_cover(self, **kwargs):
"""Open the cover."""
_LOGGER.debug("Opening garage door")
self.ryobi_door.open_device()
def update(self):
"""Update status from the door."""
_LOGGER.debug("Updating RyobiGDO status")
self.ryobi_door.update()
self._door_state = self.ryobi_door.get_door_status()

View File

@@ -5,10 +5,6 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/device_tracker.asuswrt/
"""
import logging
import re
import socket
import telnetlib
from collections import namedtuple
import voluptuous as vol
@@ -19,7 +15,7 @@ from homeassistant.const import (
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT, CONF_MODE,
CONF_PROTOCOL)
REQUIREMENTS = ['pexpect==4.6.0']
REQUIREMENTS = ['aioasuswrt==1.0.0']
_LOGGER = logging.getLogger(__name__)
@@ -44,345 +40,53 @@ PLATFORM_SCHEMA = vol.All(
}))
_LEASES_CMD = 'cat /var/lib/misc/dnsmasq.leases'
_LEASES_REGEX = re.compile(
r'\w+\s' +
r'(?P<mac>(([0-9a-f]{2}[:-]){5}([0-9a-f]{2})))\s' +
r'(?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})\s' +
r'(?P<host>([^\s]+))')
# Command to get both 5GHz and 2.4GHz clients
_WL_CMD = 'for dev in `nvram get wl_ifnames`; do wl -i $dev assoclist; done'
_WL_REGEX = re.compile(
r'\w+\s' +
r'(?P<mac>(([0-9A-F]{2}[:-]){5}([0-9A-F]{2})))')
_IP_NEIGH_CMD = 'ip neigh'
_IP_NEIGH_REGEX = re.compile(
r'(?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3}|'
r'([0-9a-fA-F]{1,4}:){1,7}[0-9a-fA-F]{0,4}(:[0-9a-fA-F]{1,4}){1,7})\s'
r'\w+\s'
r'\w+\s'
r'(\w+\s(?P<mac>(([0-9a-f]{2}[:-]){5}([0-9a-f]{2}))))?\s'
r'\s?(router)?'
r'\s?(nud)?'
r'(?P<status>(\w+))')
_ARP_CMD = 'arp -n'
_ARP_REGEX = re.compile(
r'.+\s' +
r'\((?P<ip>([0-9]{1,3}[\.]){3}[0-9]{1,3})\)\s' +
r'.+\s' +
r'(?P<mac>(([0-9a-f]{2}[:-]){5}([0-9a-f]{2})))' +
r'\s' +
r'.*')
def get_scanner(hass, config):
async def async_get_scanner(hass, config):
"""Validate the configuration and return an ASUS-WRT scanner."""
scanner = AsusWrtDeviceScanner(config[DOMAIN])
await scanner.async_connect()
return scanner if scanner.success_init else None
def _parse_lines(lines, regex):
"""Parse the lines using the given regular expression.
If a line can't be parsed it is logged and skipped in the output.
"""
results = []
for line in lines:
match = regex.search(line)
if not match:
_LOGGER.debug("Could not parse row: %s", line)
continue
results.append(match.groupdict())
return results
Device = namedtuple('Device', ['mac', 'ip', 'name'])
class AsusWrtDeviceScanner(DeviceScanner):
"""This class queries a router running ASUSWRT firmware."""
# Eighth attribute needed for mode (AP mode vs router mode)
def __init__(self, config):
"""Initialize the scanner."""
self.host = config[CONF_HOST]
self.username = config[CONF_USERNAME]
self.password = config.get(CONF_PASSWORD, '')
self.ssh_key = config.get('ssh_key', config.get('pub_key', ''))
self.protocol = config[CONF_PROTOCOL]
self.mode = config[CONF_MODE]
self.port = config[CONF_PORT]
self.require_ip = config[CONF_REQUIRE_IP]
if self.protocol == 'ssh':
self.connection = SshConnection(
self.host, self.port, self.username, self.password,
self.ssh_key)
else:
self.connection = TelnetConnection(
self.host, self.port, self.username, self.password)
from aioasuswrt.asuswrt import AsusWrt
self.last_results = {}
self.success_init = False
self.connection = AsusWrt(config[CONF_HOST], config[CONF_PORT],
config[CONF_PROTOCOL] == 'telnet',
config[CONF_USERNAME],
config.get(CONF_PASSWORD, ''),
config.get('ssh_key',
config.get('pub_key', '')),
config[CONF_MODE], config[CONF_REQUIRE_IP])
async def async_connect(self):
"""Initialize connection to the router."""
# Test the router is accessible.
data = self.get_asuswrt_data()
data = await self.connection.async_get_connected_devices()
self.success_init = data is not None
def scan_devices(self):
async def async_scan_devices(self):
"""Scan for new devices and return a list with found device IDs."""
self._update_info()
await self.async_update_info()
return list(self.last_results.keys())
def get_device_name(self, device):
async def async_get_device_name(self, device):
"""Return the name of the given device or None if we don't know."""
if device not in self.last_results:
return None
return self.last_results[device].name
def _update_info(self):
async def async_update_info(self):
"""Ensure the information from the ASUSWRT router is up to date.
Return boolean if scanning successful.
"""
if not self.success_init:
return False
_LOGGER.info('Checking Devices')
data = self.get_asuswrt_data()
if not data:
return False
self.last_results = data
return True
def get_asuswrt_data(self):
"""Retrieve data from ASUSWRT.
Calls various commands on the router and returns the superset of all
responses. Some commands will not work on some routers.
"""
devices = {}
devices.update(self._get_wl())
devices.update(self._get_arp())
devices.update(self._get_neigh(devices))
if not self.mode == 'ap':
devices.update(self._get_leases(devices))
ret_devices = {}
for key in devices:
if not self.require_ip or devices[key].ip is not None:
ret_devices[key] = devices[key]
return ret_devices
def _get_wl(self):
lines = self.connection.run_command(_WL_CMD)
if not lines:
return {}
result = _parse_lines(lines, _WL_REGEX)
devices = {}
for device in result:
mac = device['mac'].upper()
devices[mac] = Device(mac, None, None)
return devices
def _get_leases(self, cur_devices):
lines = self.connection.run_command(_LEASES_CMD)
if not lines:
return {}
lines = [line for line in lines if not line.startswith('duid ')]
result = _parse_lines(lines, _LEASES_REGEX)
devices = {}
for device in result:
# For leases where the client doesn't set a hostname, ensure it
# is blank and not '*', which breaks entity_id down the line.
host = device['host']
if host == '*':
host = ''
mac = device['mac'].upper()
if mac in cur_devices:
devices[mac] = Device(mac, device['ip'], host)
return devices
def _get_neigh(self, cur_devices):
lines = self.connection.run_command(_IP_NEIGH_CMD)
if not lines:
return {}
result = _parse_lines(lines, _IP_NEIGH_REGEX)
devices = {}
for device in result:
status = device['status']
if status is None or status.upper() != 'REACHABLE':
continue
if device['mac'] is not None:
mac = device['mac'].upper()
old_device = cur_devices.get(mac)
old_ip = old_device.ip if old_device else None
devices[mac] = Device(mac, device.get('ip', old_ip), None)
return devices
def _get_arp(self):
lines = self.connection.run_command(_ARP_CMD)
if not lines:
return {}
result = _parse_lines(lines, _ARP_REGEX)
devices = {}
for device in result:
if device['mac'] is not None:
mac = device['mac'].upper()
devices[mac] = Device(mac, device['ip'], None)
return devices
class _Connection:
def __init__(self):
self._connected = False
@property
def connected(self):
"""Return connection state."""
return self._connected
def connect(self):
"""Mark current connection state as connected."""
self._connected = True
def disconnect(self):
"""Mark current connection state as disconnected."""
self._connected = False
class SshConnection(_Connection):
"""Maintains an SSH connection to an ASUS-WRT router."""
def __init__(self, host, port, username, password, ssh_key):
"""Initialize the SSH connection properties."""
super().__init__()
self._ssh = None
self._host = host
self._port = port
self._username = username
self._password = password
self._ssh_key = ssh_key
def run_command(self, command):
"""Run commands through an SSH connection.
Connect to the SSH server if not currently connected, otherwise
use the existing connection.
"""
from pexpect import pxssh, exceptions
try:
if not self.connected:
self.connect()
self._ssh.sendline(command)
self._ssh.prompt()
lines = self._ssh.before.split(b'\n')[1:-1]
return [line.decode('utf-8') for line in lines]
except exceptions.EOF as err:
_LOGGER.error("Connection refused. %s", self._ssh.before)
self.disconnect()
return None
except pxssh.ExceptionPxssh as err:
_LOGGER.error("Unexpected SSH error: %s", err)
self.disconnect()
return None
except AssertionError as err:
_LOGGER.error("Connection to router unavailable: %s", err)
self.disconnect()
return None
def connect(self):
"""Connect to the ASUS-WRT SSH server."""
from pexpect import pxssh
self._ssh = pxssh.pxssh()
if self._ssh_key:
self._ssh.login(self._host, self._username, quiet=False,
ssh_key=self._ssh_key, port=self._port)
else:
self._ssh.login(self._host, self._username, quiet=False,
password=self._password, port=self._port)
super().connect()
def disconnect(self):
"""Disconnect the current SSH connection."""
try:
self._ssh.logout()
except Exception: # pylint: disable=broad-except
pass
finally:
self._ssh = None
super().disconnect()
class TelnetConnection(_Connection):
"""Maintains a Telnet connection to an ASUS-WRT router."""
def __init__(self, host, port, username, password):
"""Initialize the Telnet connection properties."""
super().__init__()
self._telnet = None
self._host = host
self._port = port
self._username = username
self._password = password
self._prompt_string = None
def run_command(self, command):
"""Run a command through a Telnet connection.
Connect to the Telnet server if not currently connected, otherwise
use the existing connection.
"""
try:
if not self.connected:
self.connect()
self._telnet.write('{}\n'.format(command).encode('ascii'))
data = (self._telnet.read_until(self._prompt_string).
split(b'\n')[1:-1])
return [line.decode('utf-8') for line in data]
except EOFError:
_LOGGER.error("Unexpected response from router")
self.disconnect()
return None
except ConnectionRefusedError:
_LOGGER.error("Connection refused by router. Telnet enabled?")
self.disconnect()
return None
except socket.gaierror as exc:
_LOGGER.error("Socket exception: %s", exc)
self.disconnect()
return None
except OSError as exc:
_LOGGER.error("OSError: %s", exc)
self.disconnect()
return None
def connect(self):
"""Connect to the ASUS-WRT Telnet server."""
self._telnet = telnetlib.Telnet(self._host)
self._telnet.read_until(b'login: ')
self._telnet.write((self._username + '\n').encode('ascii'))
self._telnet.read_until(b'Password: ')
self._telnet.write((self._password + '\n').encode('ascii'))
self._prompt_string = self._telnet.read_until(b'#').split(b'\n')[-1]
super().connect()
def disconnect(self):
"""Disconnect the current Telnet connection."""
try:
self._telnet.write('exit\n'.encode('ascii'))
except Exception: # pylint: disable=broad-except
pass
super().disconnect()
self.last_results = await self.connection.async_get_connected_devices()

View File

@@ -44,7 +44,10 @@ def setup_scanner(hass, config, see, discovery_info=None):
new_devices[address] = 1
return
see(mac=BLE_PREFIX + address, host_name=name.strip("\x00"),
if name is not None:
name = name.strip("\x00")
see(mac=BLE_PREFIX + address, host_name=name,
source_type=SOURCE_TYPE_BLUETOOTH_LE)
def discover_ble_devices():

View File

@@ -15,7 +15,7 @@ from homeassistant.const import (
CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT, CONF_SSL,
CONF_DEVICES, CONF_EXCLUDE)
REQUIREMENTS = ['pynetgear==0.4.2']
REQUIREMENTS = ['pynetgear==0.5.0']
_LOGGER = logging.getLogger(__name__)

View File

@@ -102,5 +102,6 @@ def setup(hass, config):
discovery.load_platform(hass, "sensor", DOMAIN, {}, config)
discovery.load_platform(hass, "fan", DOMAIN, {}, config)
discovery.load_platform(hass, "vacuum", DOMAIN, {}, config)
discovery.load_platform(hass, "climate", DOMAIN, {}, config)
return True

View File

@@ -24,7 +24,7 @@ from homeassistant.core import callback
from homeassistant.helpers.translation import async_get_translations
from homeassistant.loader import bind_hass
REQUIREMENTS = ['home-assistant-frontend==20181018.0']
REQUIREMENTS = ['home-assistant-frontend==20181023.0']
DOMAIN = 'frontend'
DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log',

View File

@@ -14,7 +14,8 @@ CONF_ROOM_HINT = 'room'
DEFAULT_EXPOSE_BY_DEFAULT = True
DEFAULT_EXPOSED_DOMAINS = [
'switch', 'light', 'group', 'media_player', 'fan', 'cover', 'climate'
'climate', 'cover', 'fan', 'group', 'input_boolean', 'light',
'media_player', 'scene', 'script', 'switch'
]
CLIMATE_MODE_HEATCOOL = 'heatcool'
CLIMATE_SUPPORTED_MODES = {'heat', 'cool', 'off', 'on', CLIMATE_MODE_HEATCOOL}

View File

@@ -14,6 +14,7 @@
"data": {
"2fa": "2FA Pin"
},
"description": "\u00dcres",
"title": "K\u00e9tfaktoros Hiteles\u00edt\u00e9s"
},
"user": {
@@ -21,6 +22,7 @@
"email": "E-Mail C\u00edm",
"password": "Jelsz\u00f3"
},
"description": "\u00dcres",
"title": "Google Hangouts Bejelentkez\u00e9s"
}
},

View File

@@ -0,0 +1,28 @@
{
"config": {
"abort": {
"already_configured": "Google Hangouts este deja configurat",
"unknown": "Sa produs o eroare necunoscut\u0103."
},
"error": {
"invalid_2fa_method": "Metoda 2FA invalid\u0103 (Verifica\u021bi pe telefon).",
"invalid_login": "Conectare invalid\u0103, \u00eencerca\u021bi din nou."
},
"step": {
"2fa": {
"data": {
"2fa": "2FA Pin"
}
},
"user": {
"data": {
"email": "Adresa de email",
"password": "Parol\u0103"
},
"description": "Gol",
"title": "Conectare Google Hangouts"
}
},
"title": "Google Hangouts"
}
}

View File

@@ -0,0 +1,19 @@
{
"config": {
"abort": {
"already_configured": "Punctul de acces este deja configurat",
"unknown": "Sa produs o eroare necunoscut\u0103."
},
"error": {
"invalid_pin": "Cod PIN invalid, \u00eencerca\u021bi din nou.",
"press_the_button": "V\u0103 rug\u0103m s\u0103 ap\u0103sa\u021bi butonul albastru."
},
"step": {
"init": {
"data": {
"pin": "Cod PIN (op\u021bional)"
}
}
}
}
}

View File

@@ -2,6 +2,8 @@
"config": {
"abort": {
"all_configured": "Toate pun\u021bile Philips Hue sunt deja configurate",
"already_configured": "Gateway-ul este deja configurat",
"cannot_connect": "Nu se poate conecta la gateway.",
"discover_timeout": "Imposibil de descoperit podurile Hue"
},
"error": {

View File

@@ -0,0 +1,11 @@
{
"config": {
"step": {
"user": {
"description": "\u00bfEst\u00e1s seguro de que quieres configurar IFTTT?",
"title": "Configurar el applet de webhook IFTTT"
}
},
"title": "IFTTT"
}
}

View File

@@ -1,5 +1,8 @@
{
"config": {
"abort": {
"not_internet_accessible": "A Home Assistant-nek el\u00e9rhet\u0151nek kell lennie az internetr\u0151l az IFTTT \u00fczenetek fogad\u00e1s\u00e1hoz."
},
"step": {
"user": {
"description": "Biztosan be szeretn\u00e9d \u00e1ll\u00edtani az IFTTT-t?",

View File

@@ -0,0 +1,18 @@
{
"config": {
"abort": {
"not_internet_accessible": "A sua inst\u00e2ncia Home Assistent precisa de ser acess\u00edvel a partir da internet para receber mensagens IFTTT.",
"one_instance_allowed": "Apenas uma \u00fanica inst\u00e2ncia \u00e9 necess\u00e1ria."
},
"create_entry": {
"default": "Para enviar eventos para o Home Assistente, precisa de utilizar a a\u00e7\u00e3o \"Make a web request\" no [IFTTT Webhook applet]({applet_url}).\n\nPreencha com a seguinte informa\u00e7\u00e3o:\n\n- URL: `{webhook_url}`\n- Method: POST \n- Content Type: application/json \n\nConsulte [a documenta\u00e7\u00e3o]({docs_url}) sobre como configurar automa\u00e7\u00f5es para lidar com dados de entrada."
},
"step": {
"user": {
"description": "Tem certeza de que deseja configurar o IFTTT?",
"title": "Configurar o IFTTT Webhook Applet"
}
},
"title": ""
}
}

View File

@@ -1,5 +1,9 @@
{
"config": {
"abort": {
"not_internet_accessible": "Instan\u021ba Home Assistant trebuie s\u0103 fie accesibil\u0103 de pe internet pentru a primi mesaje IFTTT.",
"one_instance_allowed": "Este necesar\u0103 o singur\u0103 instan\u021b\u0103."
},
"step": {
"user": {
"description": "Sigur dori\u021bi s\u0103 configura\u021bi IFTTT?"

View File

@@ -0,0 +1,5 @@
{
"config": {
"title": "IFTT"
}
}

View File

@@ -4,18 +4,15 @@ Support to trigger Maker IFTTT recipes.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/ifttt/
"""
from ipaddress import ip_address
import json
import logging
from urllib.parse import urlparse
import requests
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant import config_entries
from homeassistant.const import CONF_WEBHOOK_ID
from homeassistant.util.network import is_local
from homeassistant.helpers import config_entry_flow
REQUIREMENTS = ['pyfttt==0.3']
DEPENDENCIES = ['webhook']
@@ -100,43 +97,11 @@ async def async_unload_entry(hass, entry):
hass.components.webhook.async_unregister(entry.data[CONF_WEBHOOK_ID])
return True
@config_entries.HANDLERS.register(DOMAIN)
class ConfigFlow(config_entries.ConfigFlow):
"""Handle an IFTTT config flow."""
async def async_step_user(self, user_input=None):
"""Handle a user initiated set up flow."""
if self._async_current_entries():
return self.async_abort(reason='one_instance_allowed')
try:
url_parts = urlparse(self.hass.config.api.base_url)
if is_local(ip_address(url_parts.hostname)):
return self.async_abort(reason='not_internet_accessible')
except ValueError:
# If it's not an IP address, it's very likely publicly accessible
pass
if user_input is None:
return self.async_show_form(
step_id='user',
)
webhook_id = self.hass.components.webhook.async_generate_id()
webhook_url = \
self.hass.components.webhook.async_generate_url(webhook_id)
return self.async_create_entry(
title='IFTTT Webhook',
data={
CONF_WEBHOOK_ID: webhook_id
},
description_placeholders={
'applet_url': 'https://ifttt.com/maker_webhooks',
'webhook_url': webhook_url,
'docs_url':
'https://www.home-assistant.io/components/ifttt/'
}
)
config_entry_flow.register_webhook_flow(
DOMAIN,
'IFTTT Webhook',
{
'applet_url': 'https://ifttt.com/maker_webhooks',
'docs_url': 'https://www.home-assistant.io/components/ifttt/'
}
)

View File

@@ -0,0 +1,14 @@
{
"config": {
"abort": {
"single_instance_allowed": "Este necesar\u0103 numai o singur\u0103 configurare a aplica\u021biei Home Assistant iOS."
},
"step": {
"confirm": {
"description": "Dori\u021bi s\u0103 configura\u021bi componenta Home Assistant iOS?",
"title": "Home Assistant iOS"
}
},
"title": "Home Assistant iOS"
}
}

View File

@@ -0,0 +1,15 @@
{
"config": {
"abort": {
"no_devices_found": "No se encontraron dispositivos LIFX en la red.",
"single_instance_allowed": "S\u00f3lo es posible una \u00fanica configuraci\u00f3n de LIFX."
},
"step": {
"confirm": {
"description": "\u00bfQuieres configurar LIFX?",
"title": "LIFX"
}
},
"title": "LIFX"
}
}

View File

@@ -0,0 +1,7 @@
{
"config": {
"abort": {
"no_devices_found": "Nenhum dispositivo LIFX encontrado na rede."
}
}
}

View File

@@ -18,7 +18,7 @@ from homeassistant.components.light import (
import homeassistant.helpers.config_validation as cv
import homeassistant.util.color as color_util
REQUIREMENTS = ['flux_led==0.21']
REQUIREMENTS = ['flux_led==0.22']
_LOGGER = logging.getLogger(__name__)

View File

@@ -20,7 +20,7 @@ from homeassistant.util.color import (
color_temperature_mired_to_kelvin, color_hs_to_RGB)
from homeassistant.helpers.restore_state import async_get_last_state
REQUIREMENTS = ['limitlessled==1.1.2']
REQUIREMENTS = ['limitlessled==1.1.3']
_LOGGER = logging.getLogger(__name__)

View File

@@ -6,16 +6,18 @@ https://home-assistant.io/components/light.rflink/
"""
import logging
import voluptuous as vol
from homeassistant.components.light import (
ATTR_BRIGHTNESS, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, Light)
from homeassistant.components.rflink import (
CONF_ALIASES, CONF_ALIASSES, CONF_AUTOMATIC_ADD, CONF_DEVICE_DEFAULTS,
CONF_DEVICES, CONF_FIRE_EVENT, CONF_GROUP, CONF_GROUP_ALIASES,
CONF_GROUP_ALIASSES, CONF_IGNORE_DEVICES, CONF_NOGROUP_ALIASES,
CONF_NOGROUP_ALIASSES, CONF_SIGNAL_REPETITIONS, DATA_DEVICE_REGISTER,
DEVICE_DEFAULTS_SCHEMA,
EVENT_KEY_COMMAND, EVENT_KEY_ID, SwitchableRflinkDevice, cv,
remove_deprecated, vol)
CONF_GROUP_ALIASSES, CONF_NOGROUP_ALIASES, CONF_NOGROUP_ALIASSES,
CONF_SIGNAL_REPETITIONS, DATA_DEVICE_REGISTER, DEVICE_DEFAULTS_SCHEMA,
EVENT_KEY_COMMAND, EVENT_KEY_ID, SwitchableRflinkDevice,
remove_deprecated)
import homeassistant.helpers.config_validation as cv
from homeassistant.const import (CONF_NAME, CONF_TYPE)
DEPENDENCIES = ['rflink']
@@ -28,7 +30,6 @@ TYPE_HYBRID = 'hybrid'
TYPE_TOGGLE = 'toggle'
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_IGNORE_DEVICES): vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_DEVICE_DEFAULTS, default=DEVICE_DEFAULTS_SCHEMA({})):
DEVICE_DEFAULTS_SCHEMA,
vol.Optional(CONF_AUTOMATIC_ADD, default=True): cv.boolean,

View File

@@ -40,6 +40,7 @@ class AugustLock(LockDevice):
self._lock_status = None
self._lock_detail = None
self._changed_by = None
self._available = False
def lock(self, **kwargs):
"""Lock the device."""
@@ -52,6 +53,8 @@ class AugustLock(LockDevice):
def update(self):
"""Get the latest state of the sensor."""
self._lock_status = self._data.get_lock_status(self._lock.device_id)
self._available = self._lock_status is not None
self._lock_detail = self._data.get_lock_detail(self._lock.device_id)
from august.activity import ActivityType
@@ -67,6 +70,11 @@ class AugustLock(LockDevice):
"""Return the name of this device."""
return self._lock.device_name
@property
def available(self):
"""Return the availability of this sensor."""
return self._available
@property
def is_locked(self):
"""Return true if device is on."""

View File

@@ -2,9 +2,10 @@
import logging
import uuid
import os
from os import O_WRONLY, O_CREAT, O_TRUNC
from os import O_CREAT, O_TRUNC, O_WRONLY
from collections import OrderedDict
from typing import Union, List, Dict
from typing import Dict, List, Union
import voluptuous as vol
from homeassistant.components import websocket_api
@@ -14,21 +15,45 @@ _LOGGER = logging.getLogger(__name__)
DOMAIN = 'lovelace'
REQUIREMENTS = ['ruamel.yaml==0.15.72']
LOVELACE_CONFIG_FILE = 'ui-lovelace.yaml'
JSON_TYPE = Union[List, Dict, str] # pylint: disable=invalid-name
OLD_WS_TYPE_GET_LOVELACE_UI = 'frontend/lovelace_config'
WS_TYPE_GET_LOVELACE_UI = 'lovelace/config'
WS_TYPE_GET_CARD = 'lovelace/config/card/get'
WS_TYPE_SET_CARD = 'lovelace/config/card/set'
SCHEMA_GET_LOVELACE_UI = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): vol.Any(WS_TYPE_GET_LOVELACE_UI,
OLD_WS_TYPE_GET_LOVELACE_UI),
})
JSON_TYPE = Union[List, Dict, str] # pylint: disable=invalid-name
SCHEMA_GET_CARD = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_GET_CARD,
vol.Required('card_id'): str,
vol.Optional('format', default='yaml'): str,
})
SCHEMA_SET_CARD = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_SET_CARD,
vol.Required('card_id'): str,
vol.Required('card_config'): vol.Any(str, Dict),
vol.Optional('format', default='yaml'): str,
})
class WriteError(HomeAssistantError):
"""Error writing the data."""
class CardNotFoundError(HomeAssistantError):
"""Card not found in data."""
class UnsupportedYamlError(HomeAssistantError):
"""Unsupported YAML."""
def save_yaml(fname: str, data: JSON_TYPE):
"""Save a YAML file."""
from ruamel.yaml import YAML
@@ -45,7 +70,7 @@ def save_yaml(fname: str, data: JSON_TYPE):
_LOGGER.error(str(exc))
raise HomeAssistantError(exc)
except OSError as exc:
_LOGGER.exception('Saving YAML file failed: %s', fname)
_LOGGER.exception('Saving YAML file %s failed: %s', fname, exc)
raise WriteError(exc)
finally:
if os.path.exists(tmp_fname):
@@ -57,18 +82,29 @@ def save_yaml(fname: str, data: JSON_TYPE):
_LOGGER.error("YAML replacement cleanup failed: %s", exc)
def _yaml_unsupported(loader, node):
raise UnsupportedYamlError(
'Unsupported YAML, you can not use {} in ui-lovelace.yaml'
.format(node.tag))
def load_yaml(fname: str) -> JSON_TYPE:
"""Load a YAML file."""
from ruamel.yaml import YAML
from ruamel.yaml.constructor import RoundTripConstructor
from ruamel.yaml.error import YAMLError
RoundTripConstructor.add_constructor(None, _yaml_unsupported)
yaml = YAML(typ='rt')
try:
with open(fname, encoding='utf-8') as conf_file:
# If configuration file is empty YAML returns None
# We convert that to an empty dict
return yaml.load(conf_file) or OrderedDict()
except YAMLError as exc:
_LOGGER.error("YAML error: %s", exc)
_LOGGER.error("YAML error in %s: %s", fname, exc)
raise HomeAssistantError(exc)
except UnicodeDecodeError as exc:
_LOGGER.error("Unable to read file %s: %s", fname, exc)
@@ -76,21 +112,86 @@ def load_yaml(fname: str) -> JSON_TYPE:
def load_config(fname: str) -> JSON_TYPE:
"""Load a YAML file and adds id to card if not present."""
"""Load a YAML file and adds id to views and cards if not present."""
config = load_yaml(fname)
# Check if all cards have an ID or else add one
# Check if all views and cards have an id or else add one
updated = False
index = 0
for view in config.get('views', []):
if 'id' not in view:
updated = True
view.insert(0, 'id', index,
comment="Automatically created id")
for card in view.get('cards', []):
if 'id' not in card:
updated = True
card['id'] = uuid.uuid4().hex
card.move_to_end('id', last=False)
card.insert(0, 'id', uuid.uuid4().hex,
comment="Automatically created id")
index += 1
if updated:
save_yaml(fname, config)
return config
def object_to_yaml(data: JSON_TYPE) -> str:
"""Create yaml string from object."""
from ruamel.yaml import YAML
from ruamel.yaml.error import YAMLError
from ruamel.yaml.compat import StringIO
yaml = YAML(typ='rt')
yaml.indent(sequence=4, offset=2)
stream = StringIO()
try:
yaml.dump(data, stream)
return stream.getvalue()
except YAMLError as exc:
_LOGGER.error("YAML error: %s", exc)
raise HomeAssistantError(exc)
def yaml_to_object(data: str) -> JSON_TYPE:
"""Create object from yaml string."""
from ruamel.yaml import YAML
from ruamel.yaml.error import YAMLError
yaml = YAML(typ='rt')
try:
return yaml.load(data)
except YAMLError as exc:
_LOGGER.error("YAML error: %s", exc)
raise HomeAssistantError(exc)
def get_card(fname: str, card_id: str, data_format: str) -> JSON_TYPE:
"""Load a specific card config for id."""
config = load_yaml(fname)
for view in config.get('views', []):
for card in view.get('cards', []):
if card.get('id') == card_id:
if data_format == 'yaml':
return object_to_yaml(card)
return card
raise CardNotFoundError(
"Card with ID: {} was not found in {}.".format(card_id, fname))
def set_card(fname: str, card_id: str, card_config: str, data_format: str)\
-> bool:
"""Save a specific card config for id."""
config = load_yaml(fname)
for view in config.get('views', []):
for card in view.get('cards', []):
if card.get('id') == card_id:
if data_format == 'yaml':
card_config = yaml_to_object(card_config)
card.update(card_config)
save_yaml(fname, config)
return True
raise CardNotFoundError(
"Card with ID: {} was not found in {}.".format(card_id, fname))
async def async_setup(hass, config):
"""Set up the Lovelace commands."""
# Backwards compat. Added in 0.80. Remove after 0.85
@@ -102,6 +203,14 @@ async def async_setup(hass, config):
WS_TYPE_GET_LOVELACE_UI, websocket_lovelace_config,
SCHEMA_GET_LOVELACE_UI)
hass.components.websocket_api.async_register_command(
WS_TYPE_GET_CARD, websocket_lovelace_get_card,
SCHEMA_GET_CARD)
hass.components.websocket_api.async_register_command(
WS_TYPE_SET_CARD, websocket_lovelace_set_card,
SCHEMA_SET_CARD)
return True
@@ -111,13 +220,15 @@ async def websocket_lovelace_config(hass, connection, msg):
error = None
try:
config = await hass.async_add_executor_job(
load_config, hass.config.path('ui-lovelace.yaml'))
load_config, hass.config.path(LOVELACE_CONFIG_FILE))
message = websocket_api.result_message(
msg['id'], config
)
except FileNotFoundError:
error = ('file_not_found',
'Could not find ui-lovelace.yaml in your config dir.')
except UnsupportedYamlError as err:
error = 'unsupported_error', str(err)
except HomeAssistantError as err:
error = 'load_error', str(err)
@@ -125,3 +236,59 @@ async def websocket_lovelace_config(hass, connection, msg):
message = websocket_api.error_message(msg['id'], *error)
connection.send_message(message)
@websocket_api.async_response
async def websocket_lovelace_get_card(hass, connection, msg):
"""Send lovelace card config over websocket config."""
error = None
try:
card = await hass.async_add_executor_job(
get_card, hass.config.path(LOVELACE_CONFIG_FILE), msg['card_id'],
msg.get('format', 'yaml'))
message = websocket_api.result_message(
msg['id'], card
)
except FileNotFoundError:
error = ('file_not_found',
'Could not find ui-lovelace.yaml in your config dir.')
except UnsupportedYamlError as err:
error = 'unsupported_error', str(err)
except CardNotFoundError:
error = ('card_not_found',
'Could not find card in ui-lovelace.yaml.')
except HomeAssistantError as err:
error = 'load_error', str(err)
if error is not None:
message = websocket_api.error_message(msg['id'], *error)
connection.send_message(message)
@websocket_api.async_response
async def websocket_lovelace_set_card(hass, connection, msg):
"""Receive lovelace card config over websocket and save."""
error = None
try:
result = await hass.async_add_executor_job(
set_card, hass.config.path(LOVELACE_CONFIG_FILE),
msg['card_id'], msg['card_config'], msg.get('format', 'yaml'))
message = websocket_api.result_message(
msg['id'], result
)
except FileNotFoundError:
error = ('file_not_found',
'Could not find ui-lovelace.yaml in your config dir.')
except UnsupportedYamlError as err:
error = 'unsupported_error', str(err)
except CardNotFoundError:
error = ('card_not_found',
'Could not find card in ui-lovelace.yaml.')
except HomeAssistantError as err:
error = 'save_error', str(err)
if error is not None:
message = websocket_api.error_message(msg['id'], *error)
connection.send_message(message)

View File

@@ -1,50 +0,0 @@
"""
Support for Mailgun.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/mailgun/
"""
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.const import CONF_API_KEY, CONF_DOMAIN
from homeassistant.core import callback
from homeassistant.components.http import HomeAssistantView
DOMAIN = 'mailgun'
API_PATH = '/api/{}'.format(DOMAIN)
DATA_MAILGUN = DOMAIN
DEPENDENCIES = ['http']
MESSAGE_RECEIVED = '{}_message_received'.format(DOMAIN)
CONF_SANDBOX = 'sandbox'
DEFAULT_SANDBOX = False
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_API_KEY): cv.string,
vol.Required(CONF_DOMAIN): cv.string,
vol.Optional(CONF_SANDBOX, default=DEFAULT_SANDBOX): cv.boolean
}),
}, extra=vol.ALLOW_EXTRA)
def setup(hass, config):
"""Set up the Mailgun component."""
hass.data[DATA_MAILGUN] = config[DOMAIN]
hass.http.register_view(MailgunReceiveMessageView())
return True
class MailgunReceiveMessageView(HomeAssistantView):
"""Handle data from Mailgun inbound messages."""
url = API_PATH
name = 'api:{}'.format(DOMAIN)
@callback
def post(self, request): # pylint: disable=no-self-use
"""Handle Mailgun message POST."""
hass = request.app['hass']
data = yield from request.post()
hass.bus.async_fire(MESSAGE_RECEIVED, dict(data))

View File

@@ -0,0 +1,18 @@
{
"config": {
"abort": {
"not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive Mailgun messages.",
"one_instance_allowed": "Only a single instance is necessary."
},
"create_entry": {
"default": "To send events to Home Assistant, you will need to setup [Webhooks with Mailgun]({mailgun_url}).\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/x-www-form-urlencoded\n\nSee [the documentation]({docs_url}) on how to configure automations to handle incoming data."
},
"step": {
"user": {
"description": "Are you sure you want to set up Mailgun?",
"title": "Set up the Mailgun Webhook"
}
},
"title": "Mailgun"
}
}

View File

@@ -0,0 +1,18 @@
{
"config": {
"abort": {
"not_internet_accessible": "\u00c4r Home Assistant Instanz muss iwwert Internet accessibel si fir Mailgun Noriichten z'empf\u00e4nken.",
"one_instance_allowed": "N\u00ebmmen eng eenzeg Instanz ass n\u00e9ideg."
},
"create_entry": {
"default": "Fir Evenementer un Home Assistant ze sch\u00e9cken, mussen [Webhooks mat Mailgun]({mailgun_url}) ageriicht ginn.\n\nF\u00ebllt folgend Informatiounen aus:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/x-www-form-urlencoded\n\nLiest [Dokumentatioun]({docs_url}) w\u00e9i een Automatiounen ariicht welch eingehend Donn\u00e9\u00eb trait\u00e9ieren."
},
"step": {
"user": {
"description": "S\u00e9cher fir Mailgun anzeriichten?",
"title": "Mailgun Webhook ariichten"
}
},
"title": "Mailgun"
}
}

View File

@@ -0,0 +1,67 @@
"""
Support for Mailgun.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/mailgun/
"""
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.const import CONF_API_KEY, CONF_DOMAIN, CONF_WEBHOOK_ID
from homeassistant.helpers import config_entry_flow
DOMAIN = 'mailgun'
API_PATH = '/api/{}'.format(DOMAIN)
DEPENDENCIES = ['webhook']
MESSAGE_RECEIVED = '{}_message_received'.format(DOMAIN)
CONF_SANDBOX = 'sandbox'
DEFAULT_SANDBOX = False
CONFIG_SCHEMA = vol.Schema({
vol.Optional(DOMAIN): vol.Schema({
vol.Required(CONF_API_KEY): cv.string,
vol.Required(CONF_DOMAIN): cv.string,
vol.Optional(CONF_SANDBOX, default=DEFAULT_SANDBOX): cv.boolean,
vol.Optional(CONF_WEBHOOK_ID): cv.string,
}),
}, extra=vol.ALLOW_EXTRA)
async def async_setup(hass, config):
"""Set up the Mailgun component."""
if DOMAIN not in config:
return True
hass.data[DOMAIN] = config[DOMAIN]
return True
async def handle_webhook(hass, webhook_id, request):
"""Handle incoming webhook with Mailgun inbound messages."""
data = dict(await request.post())
data['webhook_id'] = webhook_id
hass.bus.async_fire(MESSAGE_RECEIVED, data)
async def async_setup_entry(hass, entry):
"""Configure based on config entry."""
hass.components.webhook.async_register(
entry.data[CONF_WEBHOOK_ID], handle_webhook)
return True
async def async_unload_entry(hass, entry):
"""Unload a config entry."""
hass.components.webhook.async_unregister(entry.data[CONF_WEBHOOK_ID])
return True
config_entry_flow.register_webhook_flow(
DOMAIN,
'Mailgun Webhook',
{
'mailgun_url':
'https://www.mailgun.com/blog/a-guide-to-using-mailguns-webhooks',
'docs_url': 'https://www.home-assistant.io/components/mailgun/'
}
)

View File

@@ -0,0 +1,18 @@
{
"config": {
"title": "Mailgun",
"step": {
"user": {
"title": "Set up the Mailgun Webhook",
"description": "Are you sure you want to set up Mailgun?"
}
},
"abort": {
"one_instance_allowed": "Only a single instance is necessary.",
"not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive Mailgun messages."
},
"create_entry": {
"default": "To send events to Home Assistant, you will need to setup [Webhooks with Mailgun]({mailgun_url}).\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/x-www-form-urlencoded\n\nSee [the documentation]({docs_url}) on how to configure automations to handle incoming data."
}
}
}

View File

@@ -2,6 +2,10 @@
"config": {
"step": {
"hassio_confirm": {
"data": {
"discovery": "Habilitar descubrimiento"
},
"description": "\u00bfDesea configurar Home Assistant para conectarse al agente MQTT provisto por el complemento hass.io {addon} ?",
"title": "MQTT Broker a trav\u00e9s del complemento Hass.io"
}
}

View File

@@ -10,6 +10,7 @@
"broker": {
"data": {
"broker": "Br\u00f3ker",
"discovery": "Felfedez\u00e9s enged\u00e9lyez\u00e9se",
"password": "Jelsz\u00f3",
"port": "Port",
"username": "Felhaszn\u00e1l\u00f3n\u00e9v"

View File

@@ -17,6 +17,13 @@
},
"description": "Por favor, insira os detalhes de liga\u00e7\u00e3o ao seu broker MQTT.",
"title": ""
},
"hassio_confirm": {
"data": {
"discovery": "Ativar descoberta"
},
"description": "Deseja configurar o Home Assistant para se ligar ao broker MQTT fornecido pelo add-on hass.io {addon}?",
"title": "MQTT Broker atrav\u00e9s do add-on Hass.io"
}
},
"title": ""

View File

@@ -0,0 +1,31 @@
{
"config": {
"abort": {
"single_instance_allowed": "Este permis\u0103 numai o singur\u0103 configura\u021bie de MQTT."
},
"error": {
"cannot_connect": "Imposibil de conectat la broker."
},
"step": {
"broker": {
"data": {
"broker": "Broker",
"discovery": "Activa\u021bi descoperirea",
"password": "Parol\u0103",
"port": "Port",
"username": "Nume de utilizator"
},
"description": "Introduce\u021bi informa\u021biile de conectare ale brokerului dvs. MQTT.",
"title": "MQTT"
},
"hassio_confirm": {
"data": {
"discovery": "Activa\u021bi descoperirea"
},
"description": "Dori\u021bi s\u0103 configura\u021bi Home Assistant pentru a v\u0103 conecta la brokerul MQTT furnizat de addon-ul {addon} ?",
"title": "MQTT Broker, prin intermediul Hass.io add-on"
}
},
"title": "MQTT"
}
}

View File

@@ -0,0 +1,11 @@
{
"config": {
"step": {
"hassio_confirm": {
"data": {
"discovery": "Ke\u015ffetmeyi etkinle\u015ftir"
}
}
}
}
}

View File

@@ -1,6 +1,7 @@
{
"config": {
"abort": {
"already_setup": "Csak egy Nest-fi\u00f3kot konfigur\u00e1lhat.",
"authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s az \u00e9rv\u00e9nyes\u00edt\u00e9si url gener\u00e1l\u00e1sa sor\u00e1n."
},
"error": {
@@ -18,7 +19,8 @@
"link": {
"data": {
"code": "PIN-k\u00f3d"
}
},
"title": "Nest fi\u00f3k \u00f6sszekapcsol\u00e1sa"
}
},
"title": "Nest"

View File

@@ -0,0 +1,13 @@
{
"config": {
"step": {
"link": {
"data": {
"code": "Cod PIN"
},
"title": "Leg\u0103tur\u0103 cont Nest"
}
},
"title": "Nest"
}
}

View File

@@ -7,49 +7,41 @@ https://home-assistant.io/components/notify.clicksend/
import json
import logging
from aiohttp.hdrs import CONTENT_TYPE
import requests
import voluptuous as vol
from aiohttp.hdrs import CONTENT_TYPE
import homeassistant.helpers.config_validation as cv
from homeassistant.components.notify import (
PLATFORM_SCHEMA, BaseNotificationService)
from homeassistant.const import (
CONF_API_KEY, CONF_RECIPIENT, CONF_SENDER, CONF_USERNAME,
CONTENT_TYPE_JSON)
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
BASE_API_URL = 'https://rest.clicksend.com/v3'
DEFAULT_SENDER = 'hass'
TIMEOUT = 5
HEADERS = {CONTENT_TYPE: CONTENT_TYPE_JSON}
def validate_sender(config):
"""Set the optional sender name if sender name is not provided."""
if CONF_SENDER in config:
return config
config[CONF_SENDER] = config[CONF_RECIPIENT]
return config
PLATFORM_SCHEMA = vol.Schema(
vol.All(PLATFORM_SCHEMA.extend({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_API_KEY): cv.string,
vol.Required(CONF_RECIPIENT, default=[]):
vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_SENDER): cv.string,
}), validate_sender))
vol.Optional(CONF_SENDER, default=DEFAULT_SENDER): cv.string,
}),))
def get_service(hass, config, discovery_info=None):
"""Get the ClickSend notification service."""
print("#### ", config)
if _authenticate(config) is False:
_LOGGER.exception("You are not authorized to access ClickSend")
if not _authenticate(config):
_LOGGER.error("You are not authorized to access ClickSend")
return None
return ClicksendNotificationService(config)
@@ -58,10 +50,10 @@ class ClicksendNotificationService(BaseNotificationService):
def __init__(self, config):
"""Initialize the service."""
self.username = config.get(CONF_USERNAME)
self.api_key = config.get(CONF_API_KEY)
self.recipients = config.get(CONF_RECIPIENT)
self.sender = config.get(CONF_SENDER, CONF_RECIPIENT)
self.username = config[CONF_USERNAME]
self.api_key = config[CONF_API_KEY]
self.recipients = config[CONF_RECIPIENT]
self.sender = config[CONF_SENDER]
def send_message(self, message="", **kwargs):
"""Send a message to a user."""
@@ -75,28 +67,29 @@ class ClicksendNotificationService(BaseNotificationService):
})
api_url = "{}/sms/send".format(BASE_API_URL)
resp = requests.post(
api_url, data=json.dumps(data), headers=HEADERS,
auth=(self.username, self.api_key), timeout=5)
resp = requests.post(api_url,
data=json.dumps(data),
headers=HEADERS,
auth=(self.username, self.api_key),
timeout=TIMEOUT)
if resp.status_code == 200:
return
obj = json.loads(resp.text)
response_msg = obj['response_msg']
response_code = obj['response_code']
if resp.status_code != 200:
_LOGGER.error("Error %s : %s (Code %s)", resp.status_code,
response_msg, response_code)
response_msg = obj.get('response_msg')
response_code = obj.get('response_code')
_LOGGER.error("Error %s : %s (Code %s)", resp.status_code,
response_msg, response_code)
def _authenticate(config):
"""Authenticate with ClickSend."""
api_url = '{}/account'.format(BASE_API_URL)
resp = requests.get(
api_url, headers=HEADERS, auth=(config.get(CONF_USERNAME),
config.get(CONF_API_KEY)), timeout=5)
resp = requests.get(api_url,
headers=HEADERS,
auth=(config[CONF_USERNAME],
config[CONF_API_KEY]),
timeout=TIMEOUT)
if resp.status_code != 200:
return False
return True

View File

@@ -11,10 +11,10 @@ import voluptuous as vol
from homeassistant.components.notify import (ATTR_TARGET, PLATFORM_SCHEMA,
NOTIFY_SERVICE_SCHEMA,
BaseNotificationService,
ATTR_MESSAGE)
ATTR_MESSAGE, ATTR_DATA)
from homeassistant.components.hangouts.const \
import (DOMAIN, SERVICE_SEND_MESSAGE,
import (DOMAIN, SERVICE_SEND_MESSAGE, MESSAGE_DATA_SCHEMA,
TARGETS_SCHEMA, CONF_DEFAULT_CONVERSATIONS)
_LOGGER = logging.getLogger(__name__)
@@ -26,7 +26,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
})
NOTIFY_SERVICE_SCHEMA = NOTIFY_SERVICE_SCHEMA.extend({
vol.Optional(ATTR_TARGET): [TARGETS_SCHEMA]
vol.Optional(ATTR_TARGET): [TARGETS_SCHEMA],
vol.Optional(ATTR_DATA, default={}): MESSAGE_DATA_SCHEMA
})
@@ -59,7 +60,8 @@ class HangoutsNotificationService(BaseNotificationService):
messages.append({'text': message, 'parse_str': True})
service_data = {
ATTR_TARGET: target_conversations,
ATTR_MESSAGE: messages
ATTR_MESSAGE: messages,
ATTR_DATA: kwargs[ATTR_DATA]
}
return self.hass.services.call(

View File

@@ -8,7 +8,8 @@ import logging
import voluptuous as vol
from homeassistant.components.mailgun import CONF_SANDBOX, DATA_MAILGUN
from homeassistant.components.mailgun import (
CONF_SANDBOX, DOMAIN as MAILGUN_DOMAIN)
from homeassistant.components.notify import (
PLATFORM_SCHEMA, BaseNotificationService, ATTR_TITLE, ATTR_TITLE_DEFAULT,
ATTR_DATA)
@@ -35,7 +36,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
def get_service(hass, config, discovery_info=None):
"""Get the Mailgun notification service."""
data = hass.data[DATA_MAILGUN]
data = hass.data[MAILGUN_DOMAIN]
mailgun_service = MailgunNotificationService(
data.get(CONF_DOMAIN), data.get(CONF_SANDBOX),
data.get(CONF_API_KEY), config.get(CONF_SENDER),

View File

@@ -0,0 +1,20 @@
{
"config": {
"error": {
"identifier_exists": "Coordonatele deja \u00eenregistrate",
"invalid_api_key": "Cheie API invalid\u0103"
},
"step": {
"user": {
"data": {
"api_key": "Cheie API OpenUV",
"elevation": "Altitudine",
"latitude": "Latitudine",
"longitude": "Longitudine"
},
"title": "Completa\u021bi informa\u021biile dvs."
}
},
"title": "OpenUV"
}
}

View File

@@ -210,7 +210,7 @@ class OpenUV:
if data.get('from_time') and data.get('to_time'):
self.data[DATA_PROTECTION_WINDOW] = data
else:
_LOGGER.error(
_LOGGER.debug(
'No valid protection window data for this location')
self.data[DATA_PROTECTION_WINDOW] = {}

View File

@@ -0,0 +1,6 @@
{
"state": {
"full_moon": "Lun\u0103 plin\u0103",
"new_moon": "Lun\u0103 nou\u0103"
}
}

View File

@@ -6,13 +6,16 @@ https://home-assistant.io/components/sensor.rflink/
"""
import logging
import voluptuous as vol
from homeassistant.components.rflink import (
CONF_ALIASES, CONF_ALIASSES, CONF_AUTOMATIC_ADD, CONF_DEVICES,
DATA_DEVICE_REGISTER, DATA_ENTITY_LOOKUP, EVENT_KEY_ID,
EVENT_KEY_SENSOR, EVENT_KEY_UNIT, RflinkDevice, cv, remove_deprecated, vol,
EVENT_KEY_SENSOR, EVENT_KEY_UNIT, RflinkDevice, remove_deprecated,
SIGNAL_AVAILABILITY, SIGNAL_HANDLE_EVENT, TMP_ENTITY)
from homeassistant.components.sensor import (
PLATFORM_SCHEMA)
import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, CONF_UNIT_OF_MEASUREMENT)
from homeassistant.helpers.dispatcher import (async_dispatcher_connect)

View File

@@ -0,0 +1,127 @@
"""Support for monitoring the rtorrent BitTorrent client API."""
import logging
import xmlrpc.client
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (
CONF_URL, CONF_NAME,
CONF_MONITORED_VARIABLES, STATE_IDLE)
from homeassistant.helpers.entity import Entity
import homeassistant.helpers.config_validation as cv
from homeassistant.exceptions import PlatformNotReady
_LOGGER = logging.getLogger(__name__)
SENSOR_TYPE_CURRENT_STATUS = 'current_status'
SENSOR_TYPE_DOWNLOAD_SPEED = 'download_speed'
SENSOR_TYPE_UPLOAD_SPEED = 'upload_speed'
DEFAULT_NAME = 'rtorrent'
SENSOR_TYPES = {
SENSOR_TYPE_CURRENT_STATUS: ['Status', None],
SENSOR_TYPE_DOWNLOAD_SPEED: ['Down Speed', 'kB/s'],
SENSOR_TYPE_UPLOAD_SPEED: ['Up Speed', 'kB/s'],
}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_URL): cv.url,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_MONITORED_VARIABLES, default=[]): vol.All(
cv.ensure_list, [vol.In(SENSOR_TYPES)]),
})
def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the rtorrent sensors."""
url = config[CONF_URL]
name = config[CONF_NAME]
try:
rtorrent = xmlrpc.client.ServerProxy(url)
except (xmlrpc.client.ProtocolError, ConnectionRefusedError):
_LOGGER.error("Connection to rtorrent daemon failed")
raise PlatformNotReady
dev = []
for variable in config[CONF_MONITORED_VARIABLES]:
dev.append(RTorrentSensor(variable, rtorrent, name))
add_entities(dev)
def format_speed(speed):
"""Return a bytes/s measurement as a human readable string."""
kb_spd = float(speed) / 1024
return round(kb_spd, 2 if kb_spd < 0.1 else 1)
class RTorrentSensor(Entity):
"""Representation of an rtorrent sensor."""
def __init__(self, sensor_type, rtorrent_client, client_name):
"""Initialize the sensor."""
self._name = SENSOR_TYPES[sensor_type][0]
self.client = rtorrent_client
self.type = sensor_type
self.client_name = client_name
self._state = None
self._unit_of_measurement = SENSOR_TYPES[sensor_type][1]
self.data = None
self._available = False
@property
def name(self):
"""Return the name of the sensor."""
return '{} {}'.format(self.client_name, self._name)
@property
def state(self):
"""Return the state of the sensor."""
return self._state
@property
def available(self):
"""Return true if device is available."""
return self._available
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this entity, if any."""
return self._unit_of_measurement
def update(self):
"""Get the latest data from rtorrent and updates the state."""
multicall = xmlrpc.client.MultiCall(self.client)
multicall.throttle.global_up.rate()
multicall.throttle.global_down.rate()
try:
self.data = multicall()
self._available = True
except (xmlrpc.client.ProtocolError, ConnectionRefusedError):
_LOGGER.error("Connection to rtorrent lost")
self._available = False
return
upload = self.data[0]
download = self.data[1]
if self.type == SENSOR_TYPE_CURRENT_STATUS:
if self.data:
if upload > 0 and download > 0:
self._state = 'Up/Down'
elif upload > 0 and download == 0:
self._state = 'Seeding'
elif upload == 0 and download > 0:
self._state = 'Downloading'
else:
self._state = STATE_IDLE
else:
self._state = None
if self.data:
if self.type == SENSOR_TYPE_DOWNLOAD_SPEED:
self._state = format_speed(download)
elif self.type == SENSOR_TYPE_UPLOAD_SPEED:
self._state = format_speed(upload)

View File

@@ -0,0 +1,19 @@
{
"config": {
"error": {
"identifier_exists": "Cuenta ya registrada",
"invalid_credentials": "Credenciales no v\u00e1lidas"
},
"step": {
"user": {
"data": {
"code": "C\u00f3digo (para Home Assistant)",
"password": "Contrase\u00f1a",
"username": "Direcci\u00f3n de correo electr\u00f3nico"
},
"title": "Rellene sus datos"
}
},
"title": "SimpliSafe"
}
}

View File

@@ -1,10 +1,15 @@
{
"config": {
"error": {
"invalid_credentials": "\u00c9rv\u00e9nytelen hiteles\u00edt\u0151 adatok"
},
"step": {
"user": {
"data": {
"password": "Jelsz\u00f3"
}
"password": "Jelsz\u00f3",
"username": "Email c\u00edm"
},
"title": "T\u00f6ltsd ki az adataid"
}
}
}

View File

@@ -0,0 +1,19 @@
{
"config": {
"error": {
"identifier_exists": "Account bestaat al",
"invalid_credentials": "Ongeldige gebruikersgegevens"
},
"step": {
"user": {
"data": {
"code": "Code (voor Home Assistant)",
"password": "Wachtwoord",
"username": "E-mailadres"
},
"title": "Vul uw gegevens in"
}
},
"title": "SimpliSafe"
}
}

View File

@@ -0,0 +1,19 @@
{
"config": {
"error": {
"identifier_exists": "Conta j\u00e1 registada",
"invalid_credentials": "Credenciais inv\u00e1lidas"
},
"step": {
"user": {
"data": {
"code": "C\u00f3digo (para Home Assistant)",
"password": "Palavra-passe",
"username": "Endere\u00e7o de e-mail"
},
"title": "Preencha as suas informa\u00e7\u00f5es"
}
},
"title": "SimpliSafe"
}
}

View File

@@ -7,11 +7,13 @@
"step": {
"user": {
"data": {
"code": "Cod (pentru Home Assistant)",
"password": "Parola",
"username": "Adresa de email"
},
"title": "Completa\u021bi informa\u021biile dvs."
}
}
},
"title": "SimpliSafe"
}
}

View File

@@ -0,0 +1,12 @@
{
"config": {
"step": {
"user": {
"data": {
"password": "Parola",
"username": "E-posta adresi"
}
}
}
}
}

View File

@@ -23,7 +23,7 @@ from homeassistant.helpers import config_validation as cv
from .config_flow import configured_instances
from .const import DATA_CLIENT, DEFAULT_SCAN_INTERVAL, DOMAIN, TOPIC_UPDATE
REQUIREMENTS = ['simplisafe-python==3.1.12']
REQUIREMENTS = ['simplisafe-python==3.1.13']
_LOGGER = logging.getLogger(__name__)

View File

@@ -0,0 +1,19 @@
{
"config": {
"error": {
"name_exists": "Nombre ya existe",
"wrong_location": "Ubicaci\u00f3n Suecia solamente"
},
"step": {
"user": {
"data": {
"latitude": "Latitud",
"longitude": "Longitud",
"name": "Nombre"
},
"title": "Ubicaci\u00f3n en Suecia"
}
},
"title": "Servicio meteorol\u00f3gico sueco (SMHI)"
}
}

View File

@@ -0,0 +1,12 @@
{
"config": {
"step": {
"user": {
"data": {
"latitude": "Sz\u00e9less\u00e9g",
"longitude": "Hossz\u00fas\u00e1g"
}
}
}
}
}

View File

@@ -0,0 +1,13 @@
{
"config": {
"step": {
"user": {
"data": {
"name": "Nome"
},
"title": "Localiza\u00e7\u00e3o na Su\u00e9cia"
}
},
"title": "Servi\u00e7o meteorol\u00f3gico sueco (SMHI)"
}
}

View File

@@ -1,7 +1,8 @@
{
"config": {
"error": {
"name_exists": "Numele exist\u0103 deja"
"name_exists": "Numele exist\u0103 deja",
"wrong_location": "Loca\u021bia numai \u00een Suedia"
},
"step": {
"user": {

View File

@@ -0,0 +1,16 @@
{
"config": {
"error": {
"name_exists": "Bu ad zaten var",
"wrong_location": "Konum sadece \u0130sve\u00e7"
},
"step": {
"user": {
"data": {
"latitude": "Enlem",
"longitude": "Boylam"
}
}
}
}
}

View File

@@ -0,0 +1,15 @@
{
"config": {
"abort": {
"no_devices_found": "Nu exist\u0103 dispozitive Sonos g\u0103site \u00een re\u021bea.",
"single_instance_allowed": "Este necesar\u0103 o singur\u0103 configurare a Sonos."
},
"step": {
"confirm": {
"description": "Dori\u021bi s\u0103 configura\u021bi Sonos?",
"title": "Sonos"
}
},
"title": "Sonos"
}
}

View File

@@ -6,15 +6,16 @@ https://home-assistant.io/components/switch.rflink/
"""
import logging
import voluptuous as vol
from homeassistant.components.rflink import (
CONF_ALIASES, CONF_ALIASSES, CONF_DEVICE_DEFAULTS, CONF_DEVICES,
CONF_FIRE_EVENT, CONF_GROUP, CONF_GROUP_ALIASES, CONF_GROUP_ALIASSES,
CONF_NOGROUP_ALIASES, CONF_NOGROUP_ALIASSES, CONF_SIGNAL_REPETITIONS,
DEVICE_DEFAULTS_SCHEMA, SwitchableRflinkDevice, cv,
remove_deprecated, vol)
DEVICE_DEFAULTS_SCHEMA, SwitchableRflinkDevice, remove_deprecated)
from homeassistant.components.switch import (
PLATFORM_SCHEMA, SwitchDevice)
import homeassistant.helpers.config_validation as cv
from homeassistant.const import CONF_NAME
DEPENDENCIES = ['rflink']

View File

@@ -52,6 +52,11 @@ class Switchmate(SwitchDevice):
"""Return a unique, HASS-friendly identifier for this entity."""
return self._mac.replace(':', '')
@property
def available(self) -> bool:
"""Return True if entity is available."""
return self._device.available
@property
def name(self) -> str:
"""Return the name of the switch."""

View File

@@ -0,0 +1,23 @@
{
"config": {
"abort": {
"already_configured": "Bridge-ul este deja configurat"
},
"error": {
"cannot_connect": "Nu se poate conecta la gateway.",
"invalid_key": "Nu s-a \u00eenregistrat cu cheia furnizat\u0103. Dac\u0103 acest lucru se \u00eent\u00e2mpl\u0103 \u00een continuare, \u00eencerca\u021bi s\u0103 reporni\u021bi gateway-ul.",
"timeout": "Timeout la validarea codului."
},
"step": {
"auth": {
"data": {
"host": "Gazd\u0103",
"security_code": "Cod de securitate"
},
"description": "Pute\u021bi g\u0103si codul de securitate pe spatele gateway-ului.",
"title": "Introduce\u021bi codul de securitate"
}
},
"title": "IKEA TR\u00c5DFRI"
}
}

View File

@@ -27,7 +27,7 @@ _LOGGER = logging.getLogger(__name__)
SUPPORTED_LANGUAGES = [
'ar-eg', 'ar-sa', 'ca-es', 'cs-cz', 'da-dk', 'de-at', 'de-ch', 'de-de',
'el-gr', 'en-au', 'en-ca', 'en-gb', 'en-ie', 'en-in', 'en-us', 'es-es',
'en-mx', 'fi-fi', 'fr-ca', 'fr-ch', 'fr-fr', 'he-il', 'hi-in', 'hu-hu',
'es-mx', 'fi-fi', 'fr-ca', 'fr-ch', 'fr-fr', 'he-il', 'hi-in', 'hu-hu',
'id-id', 'it-it', 'ja-jp', 'ko-kr', 'nb-no', 'nl-nl', 'pl-pl', 'pt-br',
'pt-pt', 'ro-ro', 'ru-ru', 'sk-sk', 'sv-se', 'th-th', 'tr-tr', 'zh-cn',
'zh-hk', 'zh-tw',

View File

@@ -0,0 +1,26 @@
{
"config": {
"abort": {
"already_configured": "El sitio del controlador ya est\u00e1 configurado",
"user_privilege": "El usuario debe ser administrador"
},
"error": {
"faulty_credentials": "Credenciales de usuario incorrectas",
"service_unavailable": "Servicio No disponible"
},
"step": {
"user": {
"data": {
"host": "Host",
"password": "Contrase\u00f1a",
"port": "Puerto",
"site": "ID del sitio",
"username": "Nombre de usuario",
"verify_ssl": "Controlador usando el certificado adecuado"
},
"title": "Configurar el controlador UniFi"
}
},
"title": "Controlador UniFi"
}
}

View File

@@ -1,10 +1,18 @@
{
"config": {
"abort": {
"user_privilege": "A felhaszn\u00e1l\u00f3nak rendszergazd\u00e1nak kell lennie"
},
"error": {
"faulty_credentials": "Rossz felhaszn\u00e1l\u00f3i hiteles\u00edt\u0151 adatok",
"service_unavailable": "Nincs el\u00e9rhet\u0151 szolg\u00e1ltat\u00e1s"
},
"step": {
"user": {
"data": {
"password": "Jelsz\u00f3",
"port": "Port"
"port": "Port",
"username": "Felhaszn\u00e1l\u00f3n\u00e9v"
}
}
}

View File

@@ -0,0 +1,23 @@
{
"config": {
"abort": {
"user_privilege": "Gebruiker moet beheerder zijn"
},
"error": {
"faulty_credentials": "Foutieve gebruikersgegevens",
"service_unavailable": "Geen service beschikbaar"
},
"step": {
"user": {
"data": {
"host": "Host",
"password": "Wachtwoord",
"port": "Poort",
"username": "Gebruikersnaam"
},
"title": "Stel de UniFi-controller in"
}
},
"title": "UniFi-controller"
}
}

View File

@@ -0,0 +1,26 @@
{
"config": {
"abort": {
"already_configured": "O site do controlador j\u00e1 se encontra configurado",
"user_privilege": "Utilizador tem que ser administrador"
},
"error": {
"faulty_credentials": "Credenciais do utilizador erradas",
"service_unavailable": "Nenhum servi\u00e7o dispon\u00edvel"
},
"step": {
"user": {
"data": {
"host": "Servidor",
"password": "Palavra-passe",
"port": "Porto",
"site": "Site ID",
"username": "Nome do utilizador",
"verify_ssl": "Controlador com certificados adequados"
},
"title": "Configurar o controlador UniFi"
}
},
"title": "Controlador UniFi"
}
}

View File

@@ -0,0 +1,24 @@
{
"config": {
"abort": {
"user_privilege": "Utilizatorul trebuie s\u0103 fie administrator"
},
"error": {
"faulty_credentials": "Credentiale utilizator invalide",
"service_unavailable": "Nici un serviciu disponibil"
},
"step": {
"user": {
"data": {
"host": "Gazd\u0103",
"password": "Parol\u0103",
"port": "Port",
"username": "Nume de utilizator",
"verify_ssl": "Controler utiliz\u00e2nd certificatul adecvat"
},
"title": "Configura\u021bi un controler UniFi"
}
},
"title": "Controler UniFi"
}
}

View File

@@ -0,0 +1,12 @@
{
"config": {
"step": {
"user": {
"data": {
"password": "Parola",
"username": "Kullan\u0131c\u0131 ad\u0131"
}
}
}
}
}

View File

@@ -0,0 +1,23 @@
{
"config": {
"abort": {
"already_configured": "UPnP / IGD ya est\u00e1 configurado",
"no_devices_discovered": "No se descubrieron UPnP / IGDs",
"no_sensors_or_port_mapping": "Habilitar al menos sensores o mapeo de puertos"
},
"step": {
"init": {
"title": "UPnP / IGD"
},
"user": {
"data": {
"enable_port_mapping": "Habilitar la asignaci\u00f3n de puertos para Home Assistant",
"enable_sensors": "A\u00f1adir sensores de tr\u00e1fico",
"igd": "UPnP / IGD"
},
"title": "Opciones de configuraci\u00f3n para UPnP/IGD"
}
},
"title": "UPnP / IGD"
}
}

View File

@@ -0,0 +1,16 @@
{
"config": {
"step": {
"init": {
"title": "UPnP/IGD"
},
"user": {
"data": {
"igd": "UPnP/IGD"
},
"title": "Az UPnP/IGD be\u00e1ll\u00edt\u00e1si lehet\u0151s\u00e9gei"
}
},
"title": "UPnP/IGD"
}
}

View File

@@ -0,0 +1,27 @@
{
"config": {
"abort": {
"already_configured": "UPnP/IGD j\u00e1 est\u00e1 configurado",
"no_devices_discovered": "Nenhum UPnP/IGDs descoberto",
"no_sensors_or_port_mapping": "Ative pelo menos os sensores ou o mapeamento de porta"
},
"error": {
"one": "um",
"other": "v\u00e1rios"
},
"step": {
"init": {
"title": ""
},
"user": {
"data": {
"enable_port_mapping": "Ativar o mapeamento de porta para o Home Assistant",
"enable_sensors": "Adicionar sensores de tr\u00e1fego",
"igd": ""
},
"title": "Op\u00e7\u00f5es de configura\u00e7\u00e3o para o UPnP/IGD"
}
},
"title": ""
}
}

View File

@@ -0,0 +1,11 @@
{
"config": {
"step": {
"user": {
"data": {
"enable_sensors": "Trafik sens\u00f6rleri ekleyin"
}
}
}
}
}

View File

@@ -0,0 +1,21 @@
{
"config": {
"abort": {
"one_instance_only": "El componente solo admite una instancia de Z-Wave"
},
"error": {
"option_error": "Z-Wave error de validaci\u00f3n. \u00bfLa ruta de acceso a la memoria USB escorrecta?"
},
"step": {
"user": {
"data": {
"network_key": "Clave de red (d\u00e9jelo en blanco para generar autom\u00e1ticamente)",
"usb_path": "Ruta USB"
},
"description": "Consulte https://www.home-assistant.io/docs/z-wave/installation/ para obtener informaci\u00f3n sobre las variables de configuraci\u00f3n",
"title": "Configurar Z-Wave"
}
},
"title": "Z-Wave"
}
}

View File

@@ -1,5 +1,16 @@
{
"config": {
"abort": {
"already_configured": "A Z-Wave m\u00e1r konfigur\u00e1lva van"
},
"step": {
"user": {
"data": {
"network_key": "H\u00e1l\u00f3zati kulcs (hagyja \u00fcresen az automatikus gener\u00e1l\u00e1shoz)",
"usb_path": "USB el\u00e9r\u00e9si \u00fat"
}
}
},
"title": "Z-Wave"
}
}

View File

@@ -0,0 +1,22 @@
{
"config": {
"abort": {
"already_configured": "O Z-Wave j\u00e1 est\u00e1 configurado",
"one_instance_only": "Componente suporta apenas uma inst\u00e2ncia Z-Wave"
},
"error": {
"option_error": "A valida\u00e7\u00e3o Z-Wave falhou. O caminho para o stick USB est\u00e1 correto?"
},
"step": {
"user": {
"data": {
"network_key": "Network Key (deixe em branco para auto-gera\u00e7\u00e3o)",
"usb_path": "Endere\u00e7o USB"
},
"description": "Consulte https://www.home-assistant.io/docs/z-wave/installation/ para obter informa\u00e7\u00f5es sobre as vari\u00e1veis de configura\u00e7\u00e3o",
"title": "Configurar o Z-Wave"
}
},
"title": "Z-Wave"
}
}

View File

@@ -0,0 +1,11 @@
{
"config": {
"step": {
"user": {
"data": {
"network_key": "A\u011f Anajtar\u0131 (otomatik \u00fcretilmesi i\u00e7in bo\u015f b\u0131rak\u0131n\u0131z)"
}
}
}
}
}

View File

@@ -143,6 +143,7 @@ FLOWS = [
'ifttt',
'ios',
'lifx',
'mailgun',
'mqtt',
'nest',
'openuv',

View File

@@ -129,6 +129,7 @@ CONF_SENSOR_TYPE = 'sensor_type'
CONF_SENSORS = 'sensors'
CONF_SHOW_ON_MAP = 'show_on_map'
CONF_SLAVE = 'slave'
CONF_SOURCE = 'source'
CONF_SSL = 'ssl'
CONF_STATE = 'state'
CONF_STATE_TEMPLATE = 'state_template'

View File

@@ -1,7 +1,10 @@
"""Helpers for data entry flows for config entries."""
from functools import partial
from ipaddress import ip_address
from urllib.parse import urlparse
from homeassistant import config_entries
from homeassistant.util.network import is_local
def register_discovery_flow(domain, title, discovery_function,
@@ -12,6 +15,14 @@ def register_discovery_flow(domain, title, discovery_function,
connection_class))
def register_webhook_flow(domain, title, description_placeholder,
allow_multiple=False):
"""Register flow for webhook integrations."""
config_entries.HANDLERS.register(domain)(
partial(WebhookFlowHandler, domain, title, description_placeholder,
allow_multiple))
class DiscoveryFlowHandler(config_entries.ConfigFlow):
"""Handle a discovery config flow."""
@@ -84,3 +95,50 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow):
title=self._title,
data={},
)
class WebhookFlowHandler(config_entries.ConfigFlow):
"""Handle a webhook config flow."""
VERSION = 1
def __init__(self, domain, title, description_placeholder,
allow_multiple):
"""Initialize the discovery config flow."""
self._domain = domain
self._title = title
self._description_placeholder = description_placeholder
self._allow_multiple = allow_multiple
async def async_step_user(self, user_input=None):
"""Handle a user initiated set up flow to create a webhook."""
if not self._allow_multiple and self._async_current_entries():
return self.async_abort(reason='one_instance_allowed')
try:
url_parts = urlparse(self.hass.config.api.base_url)
if is_local(ip_address(url_parts.hostname)):
return self.async_abort(reason='not_internet_accessible')
except ValueError:
# If it's not an IP address, it's very likely publicly accessible
pass
if user_input is None:
return self.async_show_form(
step_id='user',
)
webhook_id = self.hass.components.webhook.async_generate_id()
webhook_url = \
self.hass.components.webhook.async_generate_url(webhook_id)
self._description_placeholder['webhook_url'] = webhook_url
return self.async_create_entry(
title=self._title,
data={
'webhook_id': webhook_id
},
description_placeholders=self._description_placeholder
)

View File

@@ -1,6 +1,6 @@
aiohttp==3.4.4
astral==1.6.1
async_timeout==3.0.0
async_timeout==3.0.1
attrs==18.2.0
bcrypt==3.1.4
certifi>=2018.04.16

View File

@@ -4,7 +4,7 @@ from typing import Union, List, Dict
import json
import os
from os import O_WRONLY, O_CREAT, O_TRUNC
import tempfile
from homeassistant.exceptions import HomeAssistantError
@@ -46,13 +46,17 @@ def save_json(filename: str, data: Union[List, Dict],
Returns True on success.
"""
tmp_filename = filename + "__TEMP__"
tmp_filename = ""
tmp_path = os.path.split(filename)[0]
try:
json_data = json.dumps(data, sort_keys=True, indent=4)
mode = 0o600 if private else 0o644
with open(os.open(tmp_filename, O_WRONLY | O_CREAT | O_TRUNC, mode),
'w', encoding='utf-8') as fdesc:
# Modern versions of Python tempfile create this file with mode 0o600
with tempfile.NamedTemporaryFile(mode="w", encoding='utf-8',
dir=tmp_path, delete=False) as fdesc:
fdesc.write(json_data)
tmp_filename = fdesc.name
if not private:
os.chmod(tmp_filename, 0o644)
os.replace(tmp_filename, filename)
except TypeError as error:
_LOGGER.exception('Failed to serialize to JSON: %s',

8
readthedocs.yml Normal file
View File

@@ -0,0 +1,8 @@
# .readthedocs.yml
build:
image: latest
python:
version: 3.6
setup_py_install: true

View File

@@ -1,3 +1,3 @@
Sphinx==1.7.8
Sphinx==1.8.1
sphinx-autodoc-typehints==1.3.0
sphinx-autodoc-annotation==1.0.post1

View File

@@ -94,10 +94,10 @@ hbmqtt==0.9.4
hdate==0.6.5
# homeassistant.components.binary_sensor.workday
holidays==0.9.7
holidays==0.9.8
# homeassistant.components.frontend
home-assistant-frontend==20181018.0
home-assistant-frontend==20181023.0
# homeassistant.components.homematicip_cloud
homematicip==0.9.8
@@ -125,7 +125,6 @@ numpy==1.15.2
paho-mqtt==1.4.0
# homeassistant.components.device_tracker.aruba
# homeassistant.components.device_tracker.asuswrt
# homeassistant.components.device_tracker.cisco_ios
# homeassistant.components.device_tracker.unifi_direct
# homeassistant.components.media_player.pandora
@@ -222,7 +221,7 @@ ruamel.yaml==0.15.72
rxv==0.5.1
# homeassistant.components.simplisafe
simplisafe-python==3.1.12
simplisafe-python==3.1.13
# homeassistant.components.sleepiq
sleepyq==0.6

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