mirror of
https://github.com/home-assistant/core.git
synced 2025-08-10 08:05:06 +02:00
Merge branch 'dev' of https://github.com/home-assistant/home-assistant into dev
This commit is contained in:
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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)
|
||||
|
@@ -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
|
||||
|
34
homeassistant/components/auth/.translations/ro.json
Normal file
34
homeassistant/components/auth/.translations/ro.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
74
homeassistant/components/automation/geo_location.py
Normal file
74
homeassistant/components/automation/geo_location.py
Normal 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)
|
@@ -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
|
||||
|
@@ -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)
|
||||
|
||||
|
@@ -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']
|
||||
|
15
homeassistant/components/cast/.translations/ro.json
Normal file
15
homeassistant/components/cast/.translations/ro.json
Normal 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"
|
||||
}
|
||||
}
|
176
homeassistant/components/climate/dyson.py
Normal file
176
homeassistant/components/climate/dyson.py
Normal 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
|
@@ -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()
|
||||
|
@@ -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 = []
|
||||
|
@@ -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):
|
||||
|
@@ -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
|
||||
|
@@ -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):
|
||||
|
@@ -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()
|
@@ -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()
|
||||
|
@@ -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():
|
||||
|
@@ -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__)
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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',
|
||||
|
@@ -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}
|
||||
|
@@ -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"
|
||||
}
|
||||
},
|
||||
|
28
homeassistant/components/hangouts/.translations/ro.json
Normal file
28
homeassistant/components/hangouts/.translations/ro.json
Normal 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"
|
||||
}
|
||||
}
|
@@ -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)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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": {
|
||||
|
11
homeassistant/components/ifttt/.translations/es.json
Normal file
11
homeassistant/components/ifttt/.translations/es.json
Normal 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"
|
||||
}
|
||||
}
|
@@ -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?",
|
||||
|
18
homeassistant/components/ifttt/.translations/pt.json
Normal file
18
homeassistant/components/ifttt/.translations/pt.json
Normal 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": ""
|
||||
}
|
||||
}
|
@@ -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?"
|
||||
|
5
homeassistant/components/ifttt/.translations/tr.json
Normal file
5
homeassistant/components/ifttt/.translations/tr.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"config": {
|
||||
"title": "IFTT"
|
||||
}
|
||||
}
|
@@ -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/'
|
||||
}
|
||||
)
|
||||
|
14
homeassistant/components/ios/.translations/ro.json
Normal file
14
homeassistant/components/ios/.translations/ro.json
Normal 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"
|
||||
}
|
||||
}
|
15
homeassistant/components/lifx/.translations/es.json
Normal file
15
homeassistant/components/lifx/.translations/es.json
Normal 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"
|
||||
}
|
||||
}
|
7
homeassistant/components/lifx/.translations/pt.json
Normal file
7
homeassistant/components/lifx/.translations/pt.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"no_devices_found": "Nenhum dispositivo LIFX encontrado na rede."
|
||||
}
|
||||
}
|
||||
}
|
@@ -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__)
|
||||
|
||||
|
@@ -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__)
|
||||
|
||||
|
@@ -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,
|
||||
|
@@ -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."""
|
||||
|
@@ -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)
|
||||
|
@@ -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))
|
18
homeassistant/components/mailgun/.translations/en.json
Normal file
18
homeassistant/components/mailgun/.translations/en.json
Normal 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"
|
||||
}
|
||||
}
|
18
homeassistant/components/mailgun/.translations/lb.json
Normal file
18
homeassistant/components/mailgun/.translations/lb.json
Normal 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"
|
||||
}
|
||||
}
|
67
homeassistant/components/mailgun/__init__.py
Normal file
67
homeassistant/components/mailgun/__init__.py
Normal 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/'
|
||||
}
|
||||
)
|
18
homeassistant/components/mailgun/strings.json
Normal file
18
homeassistant/components/mailgun/strings.json
Normal 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."
|
||||
}
|
||||
}
|
||||
}
|
@@ -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"
|
||||
}
|
||||
}
|
||||
|
@@ -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"
|
||||
|
@@ -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": ""
|
||||
|
31
homeassistant/components/mqtt/.translations/ro.json
Normal file
31
homeassistant/components/mqtt/.translations/ro.json
Normal 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"
|
||||
}
|
||||
}
|
11
homeassistant/components/mqtt/.translations/tr.json
Normal file
11
homeassistant/components/mqtt/.translations/tr.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"hassio_confirm": {
|
||||
"data": {
|
||||
"discovery": "Ke\u015ffetmeyi etkinle\u015ftir"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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"
|
||||
|
13
homeassistant/components/nest/.translations/ro.json
Normal file
13
homeassistant/components/nest/.translations/ro.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"link": {
|
||||
"data": {
|
||||
"code": "Cod PIN"
|
||||
},
|
||||
"title": "Leg\u0103tur\u0103 cont Nest"
|
||||
}
|
||||
},
|
||||
"title": "Nest"
|
||||
}
|
||||
}
|
@@ -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
|
||||
|
@@ -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(
|
||||
|
@@ -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),
|
||||
|
20
homeassistant/components/openuv/.translations/ro.json
Normal file
20
homeassistant/components/openuv/.translations/ro.json
Normal 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"
|
||||
}
|
||||
}
|
@@ -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] = {}
|
||||
|
||||
|
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"state": {
|
||||
"full_moon": "Lun\u0103 plin\u0103",
|
||||
"new_moon": "Lun\u0103 nou\u0103"
|
||||
}
|
||||
}
|
@@ -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)
|
||||
|
127
homeassistant/components/sensor/rtorrent.py
Normal file
127
homeassistant/components/sensor/rtorrent.py
Normal 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)
|
19
homeassistant/components/simplisafe/.translations/es.json
Normal file
19
homeassistant/components/simplisafe/.translations/es.json
Normal 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"
|
||||
}
|
||||
}
|
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
19
homeassistant/components/simplisafe/.translations/nl.json
Normal file
19
homeassistant/components/simplisafe/.translations/nl.json
Normal 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"
|
||||
}
|
||||
}
|
19
homeassistant/components/simplisafe/.translations/pt.json
Normal file
19
homeassistant/components/simplisafe/.translations/pt.json
Normal 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"
|
||||
}
|
||||
}
|
@@ -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"
|
||||
}
|
||||
}
|
12
homeassistant/components/simplisafe/.translations/tr.json
Normal file
12
homeassistant/components/simplisafe/.translations/tr.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"password": "Parola",
|
||||
"username": "E-posta adresi"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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__)
|
||||
|
||||
|
19
homeassistant/components/smhi/.translations/es.json
Normal file
19
homeassistant/components/smhi/.translations/es.json
Normal 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)"
|
||||
}
|
||||
}
|
12
homeassistant/components/smhi/.translations/hu.json
Normal file
12
homeassistant/components/smhi/.translations/hu.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"latitude": "Sz\u00e9less\u00e9g",
|
||||
"longitude": "Hossz\u00fas\u00e1g"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
13
homeassistant/components/smhi/.translations/pt.json
Normal file
13
homeassistant/components/smhi/.translations/pt.json
Normal 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)"
|
||||
}
|
||||
}
|
@@ -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": {
|
||||
|
16
homeassistant/components/smhi/.translations/tr.json
Normal file
16
homeassistant/components/smhi/.translations/tr.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
15
homeassistant/components/sonos/.translations/ro.json
Normal file
15
homeassistant/components/sonos/.translations/ro.json
Normal 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"
|
||||
}
|
||||
}
|
@@ -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']
|
||||
|
@@ -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."""
|
||||
|
23
homeassistant/components/tradfri/.translations/ro.json
Normal file
23
homeassistant/components/tradfri/.translations/ro.json
Normal 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"
|
||||
}
|
||||
}
|
@@ -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',
|
||||
|
26
homeassistant/components/unifi/.translations/es.json
Normal file
26
homeassistant/components/unifi/.translations/es.json
Normal 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"
|
||||
}
|
||||
}
|
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
23
homeassistant/components/unifi/.translations/nl.json
Normal file
23
homeassistant/components/unifi/.translations/nl.json
Normal 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"
|
||||
}
|
||||
}
|
26
homeassistant/components/unifi/.translations/pt.json
Normal file
26
homeassistant/components/unifi/.translations/pt.json
Normal 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"
|
||||
}
|
||||
}
|
24
homeassistant/components/unifi/.translations/ro.json
Normal file
24
homeassistant/components/unifi/.translations/ro.json
Normal 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"
|
||||
}
|
||||
}
|
12
homeassistant/components/unifi/.translations/tr.json
Normal file
12
homeassistant/components/unifi/.translations/tr.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"password": "Parola",
|
||||
"username": "Kullan\u0131c\u0131 ad\u0131"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
23
homeassistant/components/upnp/.translations/es.json
Normal file
23
homeassistant/components/upnp/.translations/es.json
Normal 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"
|
||||
}
|
||||
}
|
16
homeassistant/components/upnp/.translations/hu.json
Normal file
16
homeassistant/components/upnp/.translations/hu.json
Normal 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"
|
||||
}
|
||||
}
|
27
homeassistant/components/upnp/.translations/pt.json
Normal file
27
homeassistant/components/upnp/.translations/pt.json
Normal 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": ""
|
||||
}
|
||||
}
|
11
homeassistant/components/upnp/.translations/tr.json
Normal file
11
homeassistant/components/upnp/.translations/tr.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"enable_sensors": "Trafik sens\u00f6rleri ekleyin"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
21
homeassistant/components/zwave/.translations/es.json
Normal file
21
homeassistant/components/zwave/.translations/es.json
Normal 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"
|
||||
}
|
||||
}
|
@@ -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"
|
||||
}
|
||||
}
|
22
homeassistant/components/zwave/.translations/pt.json
Normal file
22
homeassistant/components/zwave/.translations/pt.json
Normal 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"
|
||||
}
|
||||
}
|
11
homeassistant/components/zwave/.translations/tr.json
Normal file
11
homeassistant/components/zwave/.translations/tr.json
Normal 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)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -143,6 +143,7 @@ FLOWS = [
|
||||
'ifttt',
|
||||
'ios',
|
||||
'lifx',
|
||||
'mailgun',
|
||||
'mqtt',
|
||||
'nest',
|
||||
'openuv',
|
||||
|
@@ -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'
|
||||
|
@@ -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
|
||||
)
|
||||
|
@@ -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
|
||||
|
@@ -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
8
readthedocs.yml
Normal file
@@ -0,0 +1,8 @@
|
||||
# .readthedocs.yml
|
||||
|
||||
build:
|
||||
image: latest
|
||||
|
||||
python:
|
||||
version: 3.6
|
||||
setup_py_install: true
|
@@ -1,3 +1,3 @@
|
||||
Sphinx==1.7.8
|
||||
Sphinx==1.8.1
|
||||
sphinx-autodoc-typehints==1.3.0
|
||||
sphinx-autodoc-annotation==1.0.post1
|
||||
|
@@ -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
Reference in New Issue
Block a user