commit
2d72cff575
168 changed files with 8232 additions and 1551 deletions
11
.coveragerc
11
.coveragerc
|
@ -170,6 +170,9 @@ omit =
|
|||
homeassistant/components/tellstick.py
|
||||
homeassistant/components/*/tellstick.py
|
||||
|
||||
homeassistant/components/tesla.py
|
||||
homeassistant/components/*/tesla.py
|
||||
|
||||
homeassistant/components/*/thinkingcleaner.py
|
||||
|
||||
homeassistant/components/tradfri.py
|
||||
|
@ -328,6 +331,7 @@ omit =
|
|||
homeassistant/components/light/tplink.py
|
||||
homeassistant/components/light/tradfri.py
|
||||
homeassistant/components/light/x10.py
|
||||
homeassistant/components/light/xiaomi_philipslight.py
|
||||
homeassistant/components/light/yeelight.py
|
||||
homeassistant/components/light/yeelightsunflower.py
|
||||
homeassistant/components/light/zengge.py
|
||||
|
@ -380,6 +384,8 @@ omit =
|
|||
homeassistant/components/media_player/vlc.py
|
||||
homeassistant/components/media_player/volumio.py
|
||||
homeassistant/components/media_player/yamaha.py
|
||||
homeassistant/components/media_player/yamaha_musiccast.py
|
||||
homeassistant/components/mycroft.py
|
||||
homeassistant/components/notify/aws_lambda.py
|
||||
homeassistant/components/notify/aws_sns.py
|
||||
homeassistant/components/notify/aws_sqs.py
|
||||
|
@ -397,6 +403,7 @@ omit =
|
|||
homeassistant/components/notify/llamalab_automate.py
|
||||
homeassistant/components/notify/matrix.py
|
||||
homeassistant/components/notify/message_bird.py
|
||||
homeassistant/components/notify/mycroft.py
|
||||
homeassistant/components/notify/nfandroidtv.py
|
||||
homeassistant/components/notify/nma.py
|
||||
homeassistant/components/notify/prowl.py
|
||||
|
@ -420,6 +427,7 @@ omit =
|
|||
homeassistant/components/remote/itach.py
|
||||
homeassistant/components/scene/hunterdouglas_powerview.py
|
||||
homeassistant/components/scene/lifx_cloud.py
|
||||
homeassistant/components/sensor/airvisual.py
|
||||
homeassistant/components/sensor/arest.py
|
||||
homeassistant/components/sensor/arwn.py
|
||||
homeassistant/components/sensor/bbox.py
|
||||
|
@ -445,6 +453,7 @@ omit =
|
|||
homeassistant/components/sensor/dovado.py
|
||||
homeassistant/components/sensor/dte_energy_bridge.py
|
||||
homeassistant/components/sensor/dublin_bus_transport.py
|
||||
homeassistant/components/sensor/dwd_weather_warnings.py
|
||||
homeassistant/components/sensor/ebox.py
|
||||
homeassistant/components/sensor/eddystone_temperature.py
|
||||
homeassistant/components/sensor/eliqonline.py
|
||||
|
@ -480,6 +489,7 @@ omit =
|
|||
homeassistant/components/sensor/metoffice.py
|
||||
homeassistant/components/sensor/miflora.py
|
||||
homeassistant/components/sensor/modem_callerid.py
|
||||
homeassistant/components/sensor/mopar.py
|
||||
homeassistant/components/sensor/mqtt_room.py
|
||||
homeassistant/components/sensor/mvglive.py
|
||||
homeassistant/components/sensor/netdata.py
|
||||
|
@ -517,6 +527,7 @@ omit =
|
|||
homeassistant/components/sensor/swiss_public_transport.py
|
||||
homeassistant/components/sensor/synologydsm.py
|
||||
homeassistant/components/sensor/systemmonitor.py
|
||||
homeassistant/components/sensor/tank_utility.py
|
||||
homeassistant/components/sensor/ted5000.py
|
||||
homeassistant/components/sensor/temper.py
|
||||
homeassistant/components/sensor/time_date.py
|
||||
|
|
|
@ -33,10 +33,6 @@ of a component, check the `Home Assistant help section <https://home-assistant.i
|
|||
:target: https://coveralls.io/r/home-assistant/home-assistant?branch=master
|
||||
.. |Chat Status| image:: https://img.shields.io/discord/330944238910963714.svg
|
||||
:target: https://discord.gg/c5DvZ4e
|
||||
.. |Join the chat at https://gitter.im/home-assistant/home-assistant| image:: https://img.shields.io/badge/gitter-general-blue.svg
|
||||
:target: https://gitter.im/home-assistant/home-assistant?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
|
||||
.. |Join the dev chat at https://gitter.im/home-assistant/home-assistant/devs| image:: https://img.shields.io/badge/gitter-development-yellowgreen.svg
|
||||
:target: https://gitter.im/home-assistant/home-assistant/devs?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
|
||||
.. |screenshot-states| image:: https://raw.github.com/home-assistant/home-assistant/master/docs/screenshots.png
|
||||
:target: https://home-assistant.io/demo/
|
||||
.. |screenshot-components| image:: https://raw.github.com/home-assistant/home-assistant/dev/docs/screenshot-components.png
|
||||
|
|
|
@ -101,6 +101,12 @@ def reload_core_config(hass):
|
|||
hass.services.call(ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_reload_core_config(hass):
|
||||
"""Reload the core config."""
|
||||
yield from hass.services.async_call(ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Set up general services related to Home Assistant."""
|
||||
|
|
|
@ -4,15 +4,20 @@ This component provides basic support for Abode Home Security system.
|
|||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/abode/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
from requests.exceptions import HTTPError, ConnectTimeout
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_NAME
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.const import (ATTR_ATTRIBUTION,
|
||||
CONF_USERNAME, CONF_PASSWORD,
|
||||
CONF_NAME, EVENT_HOMEASSISTANT_STOP,
|
||||
EVENT_HOMEASSISTANT_START)
|
||||
|
||||
REQUIREMENTS = ['abodepy==0.7.1']
|
||||
REQUIREMENTS = ['abodepy==0.9.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -20,8 +25,7 @@ CONF_ATTRIBUTION = "Data provided by goabode.com"
|
|||
|
||||
DOMAIN = 'abode'
|
||||
DEFAULT_NAME = 'Abode'
|
||||
DATA_ABODE = 'data_abode'
|
||||
DEFAULT_ENTITY_NAMESPACE = 'abode'
|
||||
DATA_ABODE = 'abode'
|
||||
|
||||
NOTIFICATION_ID = 'abode_notification'
|
||||
NOTIFICATION_TITLE = 'Abode Security Setup'
|
||||
|
@ -34,19 +38,22 @@ CONFIG_SCHEMA = vol.Schema({
|
|||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
ABODE_PLATFORMS = [
|
||||
'alarm_control_panel', 'binary_sensor', 'lock', 'switch', 'cover'
|
||||
]
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Set up Abode component."""
|
||||
import abodepy
|
||||
|
||||
conf = config[DOMAIN]
|
||||
username = conf.get(CONF_USERNAME)
|
||||
password = conf.get(CONF_PASSWORD)
|
||||
|
||||
try:
|
||||
data = AbodeData(username, password)
|
||||
hass.data[DATA_ABODE] = data
|
||||
|
||||
for component in ['binary_sensor', 'alarm_control_panel']:
|
||||
discovery.load_platform(hass, component, DOMAIN, {}, config)
|
||||
hass.data[DATA_ABODE] = abode = abodepy.Abode(
|
||||
username, password, auto_login=True, get_devices=True)
|
||||
|
||||
except (ConnectTimeout, HTTPError) as ex:
|
||||
_LOGGER.error("Unable to connect to Abode: %s", str(ex))
|
||||
|
@ -58,18 +65,62 @@ def setup(hass, config):
|
|||
notification_id=NOTIFICATION_ID)
|
||||
return False
|
||||
|
||||
for platform in ABODE_PLATFORMS:
|
||||
discovery.load_platform(hass, platform, DOMAIN, {}, config)
|
||||
|
||||
def logout(event):
|
||||
"""Logout of Abode."""
|
||||
abode.stop_listener()
|
||||
abode.logout()
|
||||
_LOGGER.info("Logged out of Abode")
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, logout)
|
||||
|
||||
def startup(event):
|
||||
"""Listen for push events."""
|
||||
abode.start_listener()
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, startup)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class AbodeData:
|
||||
"""Shared Abode data."""
|
||||
class AbodeDevice(Entity):
|
||||
"""Representation of an Abode device."""
|
||||
|
||||
def __init__(self, username, password):
|
||||
"""Initialize Abode oject."""
|
||||
import abodepy
|
||||
def __init__(self, controller, device):
|
||||
"""Initialize a sensor for Abode device."""
|
||||
self._controller = controller
|
||||
self._device = device
|
||||
|
||||
self.abode = abodepy.Abode(username, password)
|
||||
self.devices = self.abode.get_devices()
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Subscribe Abode events."""
|
||||
self.hass.async_add_job(
|
||||
self._controller.register, self._device,
|
||||
self._update_callback
|
||||
)
|
||||
|
||||
_LOGGER.debug("Abode Security set up with %s devices",
|
||||
len(self.devices))
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return the polling state."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._device.name
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return {
|
||||
ATTR_ATTRIBUTION: CONF_ATTRIBUTION,
|
||||
'device_id': self._device.device_id,
|
||||
'battery_low': self._device.battery_low,
|
||||
'no_response': self._device.no_response
|
||||
}
|
||||
|
||||
def _update_callback(self, device):
|
||||
"""Update the device state."""
|
||||
self.schedule_update_ha_state()
|
||||
|
|
|
@ -6,10 +6,12 @@ https://home-assistant.io/components/alarm_control_panel.abode/
|
|||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.abode import (DATA_ABODE, DEFAULT_NAME)
|
||||
from homeassistant.const import (STATE_ALARM_ARMED_AWAY,
|
||||
from homeassistant.components.abode import (
|
||||
AbodeDevice, DATA_ABODE, DEFAULT_NAME, CONF_ATTRIBUTION)
|
||||
from homeassistant.components.alarm_control_panel import (AlarmControlPanel)
|
||||
from homeassistant.const import (ATTR_ATTRIBUTION, STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED)
|
||||
import homeassistant.components.alarm_control_panel as alarm
|
||||
|
||||
|
||||
DEPENDENCIES = ['abode']
|
||||
|
||||
|
@ -20,30 +22,19 @@ ICON = 'mdi:security'
|
|||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up a sensor for an Abode device."""
|
||||
data = hass.data.get(DATA_ABODE)
|
||||
abode = hass.data[DATA_ABODE]
|
||||
|
||||
add_devices([AbodeAlarm(hass, data, data.abode.get_alarm())])
|
||||
add_devices([AbodeAlarm(abode, abode.get_alarm())])
|
||||
|
||||
|
||||
class AbodeAlarm(alarm.AlarmControlPanel):
|
||||
class AbodeAlarm(AbodeDevice, AlarmControlPanel):
|
||||
"""An alarm_control_panel implementation for Abode."""
|
||||
|
||||
def __init__(self, hass, data, device):
|
||||
def __init__(self, controller, device):
|
||||
"""Initialize the alarm control panel."""
|
||||
super(AbodeAlarm, self).__init__()
|
||||
self._device = device
|
||||
AbodeDevice.__init__(self, controller, device)
|
||||
self._name = "{0}".format(DEFAULT_NAME)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return the polling state."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return icon."""
|
||||
|
@ -52,11 +43,11 @@ class AbodeAlarm(alarm.AlarmControlPanel):
|
|||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
if self._device.mode == "standby":
|
||||
if self._device.is_standby:
|
||||
state = STATE_ALARM_DISARMED
|
||||
elif self._device.mode == "away":
|
||||
elif self._device.is_away:
|
||||
state = STATE_ALARM_ARMED_AWAY
|
||||
elif self._device.mode == "home":
|
||||
elif self._device.is_home:
|
||||
state = STATE_ALARM_ARMED_HOME
|
||||
else:
|
||||
state = None
|
||||
|
@ -65,18 +56,21 @@ class AbodeAlarm(alarm.AlarmControlPanel):
|
|||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
self._device.set_standby()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
self._device.set_home()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
self._device.set_away()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def update(self):
|
||||
"""Update the device state."""
|
||||
self._device.refresh()
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return {
|
||||
ATTR_ATTRIBUTION: CONF_ATTRIBUTION,
|
||||
'device_id': self._device.device_id,
|
||||
'battery_backup': self._device.battery,
|
||||
'cellular_backup': self._device.is_cellular
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ from homeassistant.const import (
|
|||
CONF_NAME, STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_ARMED_AWAY, STATE_ALARM_TRIGGERED)
|
||||
|
||||
REQUIREMENTS = ['pythonegardia==1.0.18']
|
||||
REQUIREMENTS = ['pythonegardia==1.0.20']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -29,7 +29,7 @@ CONF_REPORT_SERVER_PORT = 'report_server_port'
|
|||
DEFAULT_NAME = 'Egardia'
|
||||
DEFAULT_PORT = 80
|
||||
DEFAULT_REPORT_SERVER_ENABLED = False
|
||||
DEFAULT_REPORT_SERVER_PORT = 85
|
||||
DEFAULT_REPORT_SERVER_PORT = 52010
|
||||
DOMAIN = 'egardia'
|
||||
|
||||
NOTIFICATION_ID = 'egardia_notification'
|
||||
|
@ -154,8 +154,9 @@ class EgardiaAlarm(alarm.AlarmControlPanel):
|
|||
|
||||
def update(self):
|
||||
"""Update the alarm status."""
|
||||
status = self._egardiasystem.getstate()
|
||||
self.parsestatus(status)
|
||||
if not self._rs_enabled:
|
||||
status = self._egardiasystem.getstate()
|
||||
self.parsestatus(status)
|
||||
|
||||
def alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
|
|
|
@ -24,6 +24,8 @@ DEFAULT_PENDING_TIME = 60
|
|||
DEFAULT_TRIGGER_TIME = 120
|
||||
DEFAULT_DISARM_AFTER_TRIGGER = False
|
||||
|
||||
ATTR_POST_PENDING_STATE = 'post_pending_state'
|
||||
|
||||
PLATFORM_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): 'manual',
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_ALARM_NAME): cv.string,
|
||||
|
@ -101,7 +103,9 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
|||
self._trigger_time) < dt_util.utcnow():
|
||||
if self._disarm_after_trigger:
|
||||
return STATE_ALARM_DISARMED
|
||||
return self._pre_trigger_state
|
||||
else:
|
||||
self._state = self._pre_trigger_state
|
||||
return self._state
|
||||
|
||||
return self._state
|
||||
|
||||
|
@ -183,3 +187,13 @@ class ManualAlarm(alarm.AlarmControlPanel):
|
|||
if not check:
|
||||
_LOGGER.warning("Invalid code given for %s", state)
|
||||
return check
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
state_attr = {}
|
||||
|
||||
if self.state == STATE_ALARM_PENDING:
|
||||
state_attr[ATTR_POST_PENDING_STATE] = self._state
|
||||
|
||||
return state_attr
|
||||
|
|
|
@ -12,16 +12,18 @@ import voluptuous as vol
|
|||
from homeassistant.core import callback
|
||||
from homeassistant.const import (
|
||||
CONF_VALUE_TEMPLATE, CONF_PLATFORM, CONF_ENTITY_ID,
|
||||
CONF_BELOW, CONF_ABOVE)
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
CONF_BELOW, CONF_ABOVE, CONF_FOR)
|
||||
from homeassistant.helpers.event import (
|
||||
async_track_state_change, async_track_same_state)
|
||||
from homeassistant.helpers import condition, config_validation as cv
|
||||
|
||||
TRIGGER_SCHEMA = vol.All(vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): 'numeric_state',
|
||||
vol.Required(CONF_ENTITY_ID): cv.entity_ids,
|
||||
CONF_BELOW: vol.Coerce(float),
|
||||
CONF_ABOVE: vol.Coerce(float),
|
||||
vol.Optional(CONF_BELOW): vol.Coerce(float),
|
||||
vol.Optional(CONF_ABOVE): vol.Coerce(float),
|
||||
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_FOR): vol.All(cv.time_period, cv.positive_timedelta),
|
||||
}), cv.has_at_least_one_key(CONF_BELOW, CONF_ABOVE))
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
@ -33,15 +35,18 @@ def async_trigger(hass, config, action):
|
|||
entity_id = config.get(CONF_ENTITY_ID)
|
||||
below = config.get(CONF_BELOW)
|
||||
above = config.get(CONF_ABOVE)
|
||||
time_delta = config.get(CONF_FOR)
|
||||
value_template = config.get(CONF_VALUE_TEMPLATE)
|
||||
async_remove_track_same = None
|
||||
|
||||
if value_template is not None:
|
||||
value_template.hass = hass
|
||||
|
||||
@callback
|
||||
def state_automation_listener(entity, from_s, to_s):
|
||||
"""Listen for state changes and calls action."""
|
||||
def check_numeric_state(entity, from_s, to_s):
|
||||
"""Return True if they should trigger."""
|
||||
if to_s is None:
|
||||
return
|
||||
return False
|
||||
|
||||
variables = {
|
||||
'trigger': {
|
||||
|
@ -55,17 +60,56 @@ def async_trigger(hass, config, action):
|
|||
# If new one doesn't match, nothing to do
|
||||
if not condition.async_numeric_state(
|
||||
hass, to_s, below, above, value_template, variables):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@callback
|
||||
def state_automation_listener(entity, from_s, to_s):
|
||||
"""Listen for state changes and calls action."""
|
||||
nonlocal async_remove_track_same
|
||||
|
||||
if not check_numeric_state(entity, from_s, to_s):
|
||||
return
|
||||
|
||||
variables = {
|
||||
'trigger': {
|
||||
'platform': 'numeric_state',
|
||||
'entity_id': entity,
|
||||
'below': below,
|
||||
'above': above,
|
||||
'from_state': from_s,
|
||||
'to_state': to_s,
|
||||
}
|
||||
}
|
||||
|
||||
# Only match if old didn't exist or existed but didn't match
|
||||
# Written as: skip if old one did exist and matched
|
||||
if from_s is not None and condition.async_numeric_state(
|
||||
hass, from_s, below, above, value_template, variables):
|
||||
return
|
||||
|
||||
variables['trigger']['from_state'] = from_s
|
||||
variables['trigger']['to_state'] = to_s
|
||||
@callback
|
||||
def call_action():
|
||||
"""Call action with right context."""
|
||||
hass.async_run_job(action, variables)
|
||||
|
||||
hass.async_run_job(action, variables)
|
||||
if not time_delta:
|
||||
call_action()
|
||||
return
|
||||
|
||||
return async_track_state_change(hass, entity_id, state_automation_listener)
|
||||
async_remove_track_same = async_track_same_state(
|
||||
hass, True, time_delta, call_action, entity_ids=entity_id,
|
||||
async_check_func=check_numeric_state)
|
||||
|
||||
unsub = async_track_state_change(
|
||||
hass, entity_id, state_automation_listener)
|
||||
|
||||
@callback
|
||||
def async_remove():
|
||||
"""Remove state listeners async."""
|
||||
unsub()
|
||||
if async_remove_track_same:
|
||||
async_remove_track_same() # pylint: disable=not-callable
|
||||
|
||||
return async_remove
|
||||
|
|
|
@ -8,28 +8,23 @@ import asyncio
|
|||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.const import MATCH_ALL, CONF_PLATFORM
|
||||
from homeassistant.const import MATCH_ALL, CONF_PLATFORM, CONF_FOR
|
||||
from homeassistant.helpers.event import (
|
||||
async_track_state_change, async_track_point_in_utc_time)
|
||||
async_track_state_change, async_track_same_state)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
CONF_ENTITY_ID = 'entity_id'
|
||||
CONF_FROM = 'from'
|
||||
CONF_TO = 'to'
|
||||
CONF_FOR = 'for'
|
||||
|
||||
TRIGGER_SCHEMA = vol.All(
|
||||
vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): 'state',
|
||||
vol.Required(CONF_ENTITY_ID): cv.entity_ids,
|
||||
# These are str on purpose. Want to catch YAML conversions
|
||||
CONF_FROM: str,
|
||||
CONF_TO: str,
|
||||
CONF_FOR: vol.All(cv.time_period, cv.positive_timedelta),
|
||||
}),
|
||||
cv.key_dependency(CONF_FOR, CONF_TO),
|
||||
)
|
||||
TRIGGER_SCHEMA = vol.All(vol.Schema({
|
||||
vol.Required(CONF_PLATFORM): 'state',
|
||||
vol.Required(CONF_ENTITY_ID): cv.entity_ids,
|
||||
# These are str on purpose. Want to catch YAML conversions
|
||||
vol.Optional(CONF_FROM): str,
|
||||
vol.Optional(CONF_TO): str,
|
||||
vol.Optional(CONF_FOR): vol.All(cv.time_period, cv.positive_timedelta),
|
||||
}), cv.key_dependency(CONF_FOR, CONF_TO))
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
|
@ -39,28 +34,15 @@ def async_trigger(hass, config, action):
|
|||
from_state = config.get(CONF_FROM, MATCH_ALL)
|
||||
to_state = config.get(CONF_TO, MATCH_ALL)
|
||||
time_delta = config.get(CONF_FOR)
|
||||
async_remove_state_for_cancel = None
|
||||
async_remove_state_for_listener = None
|
||||
match_all = (from_state == MATCH_ALL and to_state == MATCH_ALL)
|
||||
|
||||
@callback
|
||||
def clear_listener():
|
||||
"""Clear all unsub listener."""
|
||||
nonlocal async_remove_state_for_cancel, async_remove_state_for_listener
|
||||
|
||||
# pylint: disable=not-callable
|
||||
if async_remove_state_for_listener is not None:
|
||||
async_remove_state_for_listener()
|
||||
async_remove_state_for_listener = None
|
||||
if async_remove_state_for_cancel is not None:
|
||||
async_remove_state_for_cancel()
|
||||
async_remove_state_for_cancel = None
|
||||
async_remove_track_same = None
|
||||
|
||||
@callback
|
||||
def state_automation_listener(entity, from_s, to_s):
|
||||
"""Listen for state changes and calls action."""
|
||||
nonlocal async_remove_state_for_cancel, async_remove_state_for_listener
|
||||
nonlocal async_remove_track_same
|
||||
|
||||
@callback
|
||||
def call_action():
|
||||
"""Call action with right context."""
|
||||
hass.async_run_job(action, {
|
||||
|
@ -78,33 +60,12 @@ def async_trigger(hass, config, action):
|
|||
from_s.last_changed == to_s.last_changed):
|
||||
return
|
||||
|
||||
if time_delta is None:
|
||||
if not time_delta:
|
||||
call_action()
|
||||
return
|
||||
|
||||
@callback
|
||||
def state_for_listener(now):
|
||||
"""Fire on state changes after a delay and calls action."""
|
||||
nonlocal async_remove_state_for_listener
|
||||
async_remove_state_for_listener = None
|
||||
clear_listener()
|
||||
call_action()
|
||||
|
||||
@callback
|
||||
def state_for_cancel_listener(entity, inner_from_s, inner_to_s):
|
||||
"""Fire on changes and cancel for listener if changed."""
|
||||
if inner_to_s.state == to_s.state:
|
||||
return
|
||||
clear_listener()
|
||||
|
||||
# cleanup previous listener
|
||||
clear_listener()
|
||||
|
||||
async_remove_state_for_listener = async_track_point_in_utc_time(
|
||||
hass, state_for_listener, dt_util.utcnow() + time_delta)
|
||||
|
||||
async_remove_state_for_cancel = async_track_state_change(
|
||||
hass, entity, state_for_cancel_listener)
|
||||
async_remove_track_same = async_track_same_state(
|
||||
hass, to_s.state, time_delta, call_action, entity_ids=entity_id)
|
||||
|
||||
unsub = async_track_state_change(
|
||||
hass, entity_id, state_automation_listener, from_state, to_state)
|
||||
|
@ -113,6 +74,7 @@ def async_trigger(hass, config, action):
|
|||
def async_remove():
|
||||
"""Remove state listeners async."""
|
||||
unsub()
|
||||
clear_listener()
|
||||
if async_remove_track_same:
|
||||
async_remove_track_same() # pylint: disable=not-callable
|
||||
|
||||
return async_remove
|
||||
|
|
|
@ -6,76 +6,56 @@ https://home-assistant.io/components/binary_sensor.abode/
|
|||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.abode import (CONF_ATTRIBUTION, DATA_ABODE)
|
||||
from homeassistant.const import (ATTR_ATTRIBUTION)
|
||||
from homeassistant.components.binary_sensor import (BinarySensorDevice)
|
||||
from homeassistant.components.abode import AbodeDevice, DATA_ABODE
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
|
||||
|
||||
DEPENDENCIES = ['abode']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Sensor types: Name, device_class
|
||||
SENSOR_TYPES = {
|
||||
'Door Contact': 'opening',
|
||||
'Motion Camera': 'motion',
|
||||
}
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up a sensor for an Abode device."""
|
||||
data = hass.data.get(DATA_ABODE)
|
||||
abode = hass.data[DATA_ABODE]
|
||||
|
||||
device_types = map_abode_device_class().keys()
|
||||
|
||||
sensors = []
|
||||
for sensor in data.devices:
|
||||
_LOGGER.debug('Sensor type %s', sensor.type)
|
||||
if sensor.type in ['Door Contact', 'Motion Camera']:
|
||||
sensors.append(AbodeBinarySensor(hass, data, sensor))
|
||||
for sensor in abode.get_devices(type_filter=device_types):
|
||||
sensors.append(AbodeBinarySensor(abode, sensor))
|
||||
|
||||
_LOGGER.debug('Adding %d sensors', len(sensors))
|
||||
add_devices(sensors)
|
||||
|
||||
|
||||
class AbodeBinarySensor(BinarySensorDevice):
|
||||
def map_abode_device_class():
|
||||
"""Map Abode device types to Home Assistant binary sensor class."""
|
||||
import abodepy.helpers.constants as CONST
|
||||
|
||||
return {
|
||||
CONST.DEVICE_GLASS_BREAK: 'connectivity',
|
||||
CONST.DEVICE_KEYPAD: 'connectivity',
|
||||
CONST.DEVICE_DOOR_CONTACT: 'opening',
|
||||
CONST.DEVICE_STATUS_DISPLAY: 'connectivity',
|
||||
CONST.DEVICE_MOTION_CAMERA: 'connectivity',
|
||||
CONST.DEVICE_WATER_SENSOR: 'moisture'
|
||||
}
|
||||
|
||||
|
||||
class AbodeBinarySensor(AbodeDevice, BinarySensorDevice):
|
||||
"""A binary sensor implementation for Abode device."""
|
||||
|
||||
def __init__(self, hass, data, device):
|
||||
def __init__(self, controller, device):
|
||||
"""Initialize a sensor for Abode device."""
|
||||
super(AbodeBinarySensor, self).__init__()
|
||||
self._device = device
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return the polling state."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return "{0} {1}".format(self._device.type, self._device.name)
|
||||
AbodeDevice.__init__(self, controller, device)
|
||||
self._device_class = map_abode_device_class().get(self._device.type)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return True if the binary sensor is on."""
|
||||
if self._device.type == 'Door Contact':
|
||||
return self._device.status != 'Closed'
|
||||
elif self._device.type == 'Motion Camera':
|
||||
return self._device.get_value('motion_event') == '1'
|
||||
return self._device.is_on
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of the binary sensor."""
|
||||
return SENSOR_TYPES.get(self._device.type)
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
attrs = {}
|
||||
attrs[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION
|
||||
attrs['device_id'] = self._device.device_id
|
||||
attrs['battery_low'] = self._device.battery_low
|
||||
|
||||
return attrs
|
||||
|
||||
def update(self):
|
||||
"""Update the device state."""
|
||||
self._device.refresh()
|
||||
return self._device_class
|
||||
|
|
211
homeassistant/components/binary_sensor/bayesian.py
Normal file
211
homeassistant/components/binary_sensor/bayesian.py
Normal file
|
@ -0,0 +1,211 @@
|
|||
"""
|
||||
Use Bayesian Inference to trigger a binary sensor.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.bayesian/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
from collections import OrderedDict
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.const import (
|
||||
CONF_ABOVE, CONF_BELOW, CONF_DEVICE_CLASS, CONF_ENTITY_ID, CONF_NAME,
|
||||
CONF_PLATFORM, CONF_STATE, STATE_UNKNOWN)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import condition
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_OBSERVATIONS = 'observations'
|
||||
CONF_PRIOR = 'prior'
|
||||
CONF_PROBABILITY_THRESHOLD = 'probability_threshold'
|
||||
CONF_P_GIVEN_F = 'prob_given_false'
|
||||
CONF_P_GIVEN_T = 'prob_given_true'
|
||||
CONF_TO_STATE = 'to_state'
|
||||
|
||||
DEFAULT_NAME = 'BayesianBinary'
|
||||
|
||||
NUMERIC_STATE_SCHEMA = vol.Schema({
|
||||
CONF_PLATFORM: 'numeric_state',
|
||||
vol.Required(CONF_ENTITY_ID): cv.entity_id,
|
||||
vol.Optional(CONF_ABOVE): vol.Coerce(float),
|
||||
vol.Optional(CONF_BELOW): vol.Coerce(float),
|
||||
vol.Required(CONF_P_GIVEN_T): vol.Coerce(float),
|
||||
vol.Optional(CONF_P_GIVEN_F): vol.Coerce(float)
|
||||
}, required=True)
|
||||
|
||||
STATE_SCHEMA = vol.Schema({
|
||||
CONF_PLATFORM: CONF_STATE,
|
||||
vol.Required(CONF_ENTITY_ID): cv.entity_id,
|
||||
vol.Required(CONF_TO_STATE): cv.string,
|
||||
vol.Required(CONF_P_GIVEN_T): vol.Coerce(float),
|
||||
vol.Optional(CONF_P_GIVEN_F): vol.Coerce(float)
|
||||
}, required=True)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME):
|
||||
cv.string,
|
||||
vol.Optional(CONF_DEVICE_CLASS): cv.string,
|
||||
vol.Required(CONF_OBSERVATIONS): vol.Schema(
|
||||
vol.All(cv.ensure_list, [vol.Any(NUMERIC_STATE_SCHEMA,
|
||||
STATE_SCHEMA)])
|
||||
),
|
||||
vol.Required(CONF_PRIOR): vol.Coerce(float),
|
||||
vol.Optional(CONF_PROBABILITY_THRESHOLD):
|
||||
vol.Coerce(float),
|
||||
})
|
||||
|
||||
|
||||
def update_probability(prior, prob_true, prob_false):
|
||||
"""Update probability using Bayes' rule."""
|
||||
numerator = prob_true * prior
|
||||
denominator = numerator + prob_false * (1 - prior)
|
||||
|
||||
probability = numerator / denominator
|
||||
return probability
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up the Threshold sensor."""
|
||||
name = config.get(CONF_NAME)
|
||||
observations = config.get(CONF_OBSERVATIONS)
|
||||
prior = config.get(CONF_PRIOR)
|
||||
probability_threshold = config.get(CONF_PROBABILITY_THRESHOLD, 0.5)
|
||||
device_class = config.get(CONF_DEVICE_CLASS)
|
||||
|
||||
async_add_devices([
|
||||
BayesianBinarySensor(name, prior, observations, probability_threshold,
|
||||
device_class)
|
||||
], True)
|
||||
|
||||
|
||||
class BayesianBinarySensor(BinarySensorDevice):
|
||||
"""Representation of a Bayesian sensor."""
|
||||
|
||||
def __init__(self, name, prior, observations, probability_threshold,
|
||||
device_class):
|
||||
"""Initialize the Bayesian sensor."""
|
||||
self._name = name
|
||||
self._observations = observations
|
||||
self._probability_threshold = probability_threshold
|
||||
self._device_class = device_class
|
||||
self._deviation = False
|
||||
self.prior = prior
|
||||
self.probability = prior
|
||||
|
||||
self.current_obs = OrderedDict({})
|
||||
|
||||
self.entity_obs = {obs['entity_id']: obs for obs in self._observations}
|
||||
|
||||
self.watchers = {
|
||||
'numeric_state': self._process_numeric_state,
|
||||
'state': self._process_state
|
||||
}
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Call when entity about to be added to hass."""
|
||||
@callback
|
||||
# pylint: disable=invalid-name
|
||||
def async_threshold_sensor_state_listener(entity, old_state,
|
||||
new_state):
|
||||
"""Handle sensor state changes."""
|
||||
if new_state.state == STATE_UNKNOWN:
|
||||
return
|
||||
|
||||
entity_obs = self.entity_obs[entity]
|
||||
platform = entity_obs['platform']
|
||||
|
||||
self.watchers[platform](entity_obs)
|
||||
|
||||
prior = self.prior
|
||||
print(self.current_obs.values())
|
||||
for obs in self.current_obs.values():
|
||||
prior = update_probability(prior, obs['prob_true'],
|
||||
obs['prob_false'])
|
||||
|
||||
self.probability = prior
|
||||
|
||||
self.hass.async_add_job(self.async_update_ha_state, True)
|
||||
|
||||
entities = [obs['entity_id'] for obs in self._observations]
|
||||
async_track_state_change(
|
||||
self.hass, entities, async_threshold_sensor_state_listener)
|
||||
|
||||
def _update_current_obs(self, entity_observation, should_trigger):
|
||||
"""Update current observation."""
|
||||
entity = entity_observation['entity_id']
|
||||
|
||||
if should_trigger:
|
||||
prob_true = entity_observation['prob_given_true']
|
||||
prob_false = entity_observation.get(
|
||||
'prob_given_false', 1 - prob_true)
|
||||
|
||||
self.current_obs[entity] = {
|
||||
'prob_true': prob_true,
|
||||
'prob_false': prob_false
|
||||
}
|
||||
|
||||
else:
|
||||
self.current_obs.pop(entity, None)
|
||||
|
||||
def _process_numeric_state(self, entity_observation):
|
||||
"""Add entity to current_obs if numeric state conditions are met."""
|
||||
entity = entity_observation['entity_id']
|
||||
|
||||
should_trigger = condition.async_numeric_state(
|
||||
self.hass, entity,
|
||||
entity_observation.get('below'),
|
||||
entity_observation.get('above'), None, entity_observation)
|
||||
|
||||
self._update_current_obs(entity_observation, should_trigger)
|
||||
|
||||
def _process_state(self, entity_observation):
|
||||
"""Add entity to current observations if state conditions are met."""
|
||||
entity = entity_observation['entity_id']
|
||||
|
||||
should_trigger = condition.state(
|
||||
self.hass, entity, entity_observation.get('to_state'))
|
||||
|
||||
self._update_current_obs(entity_observation, should_trigger)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if sensor is on."""
|
||||
return self._deviation
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the sensor class of the sensor."""
|
||||
return self._device_class
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes of the sensor."""
|
||||
return {
|
||||
'observations': [val for val in self.current_obs.values()],
|
||||
'probability': self.probability,
|
||||
'probability_threshold': self._probability_threshold
|
||||
}
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
"""Get the latest data and update the states."""
|
||||
self._deviation = bool(self.probability > self._probability_threshold)
|
|
@ -18,7 +18,7 @@ from homeassistant.const import (
|
|||
CONF_SSL, EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START,
|
||||
ATTR_LAST_TRIP_TIME, CONF_CUSTOMIZE)
|
||||
|
||||
REQUIREMENTS = ['pyhik==0.1.3']
|
||||
REQUIREMENTS = ['pyhik==0.1.4']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_IGNORED = 'ignored'
|
||||
|
@ -47,6 +47,7 @@ DEVICE_CLASS_MAP = {
|
|||
'PIR Alarm': 'motion',
|
||||
'Face Detection': 'motion',
|
||||
'Scene Change Detection': 'motion',
|
||||
'I/O': None,
|
||||
}
|
||||
|
||||
CUSTOMIZE_SCHEMA = vol.Schema({
|
||||
|
|
|
@ -35,8 +35,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
|
||||
devices = []
|
||||
for conf in discovery_info[ATTR_DISCOVER_DEVICES]:
|
||||
new_device = HMBinarySensor(hass, conf)
|
||||
new_device.link_homematic()
|
||||
new_device = HMBinarySensor(conf)
|
||||
devices.append(new_device)
|
||||
|
||||
add_devices(devices)
|
||||
|
|
|
@ -1,21 +1,145 @@
|
|||
"""
|
||||
Contains functionality to use a KNX group address as a binary.
|
||||
Support for KNX/IP binary sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.knx/
|
||||
"""
|
||||
from homeassistant.components.binary_sensor import BinarySensorDevice
|
||||
from homeassistant.components.knx import (KNXConfig, KNXGroupAddress)
|
||||
import asyncio
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.knx import DATA_KNX, ATTR_DISCOVER_DEVICES, \
|
||||
KNXAutomation
|
||||
from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, \
|
||||
BinarySensorDevice
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
CONF_ADDRESS = 'address'
|
||||
CONF_DEVICE_CLASS = 'device_class'
|
||||
CONF_SIGNIFICANT_BIT = 'significant_bit'
|
||||
CONF_DEFAULT_SIGNIFICANT_BIT = 1
|
||||
CONF_AUTOMATION = 'automation'
|
||||
CONF_HOOK = 'hook'
|
||||
CONF_DEFAULT_HOOK = 'on'
|
||||
CONF_COUNTER = 'counter'
|
||||
CONF_DEFAULT_COUNTER = 1
|
||||
CONF_ACTION = 'action'
|
||||
|
||||
CONF__ACTION = 'turn_off_action'
|
||||
|
||||
DEFAULT_NAME = 'KNX Binary Sensor'
|
||||
DEPENDENCIES = ['knx']
|
||||
|
||||
AUTOMATION_SCHEMA = vol.Schema({
|
||||
vol.Optional(CONF_HOOK, default=CONF_DEFAULT_HOOK): cv.string,
|
||||
vol.Optional(CONF_COUNTER, default=CONF_DEFAULT_COUNTER): cv.port,
|
||||
vol.Required(CONF_ACTION, default=None): cv.SCRIPT_SCHEMA
|
||||
})
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the KNX binary sensor platform."""
|
||||
add_devices([KNXSwitch(hass, KNXConfig(config))])
|
||||
AUTOMATIONS_SCHEMA = vol.All(
|
||||
cv.ensure_list,
|
||||
[AUTOMATION_SCHEMA]
|
||||
)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_ADDRESS): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_DEVICE_CLASS): cv.string,
|
||||
vol.Optional(CONF_SIGNIFICANT_BIT, default=CONF_DEFAULT_SIGNIFICANT_BIT):
|
||||
cv.positive_int,
|
||||
vol.Optional(CONF_AUTOMATION, default=None): AUTOMATIONS_SCHEMA,
|
||||
})
|
||||
|
||||
|
||||
class KNXSwitch(KNXGroupAddress, BinarySensorDevice):
|
||||
"""Representation of a KNX binary sensor device."""
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, add_devices,
|
||||
discovery_info=None):
|
||||
"""Set up binary sensor(s) for KNX platform."""
|
||||
if DATA_KNX not in hass.data \
|
||||
or not hass.data[DATA_KNX].initialized:
|
||||
return False
|
||||
|
||||
pass
|
||||
if discovery_info is not None:
|
||||
async_add_devices_discovery(hass, discovery_info, add_devices)
|
||||
else:
|
||||
async_add_devices_config(hass, config, add_devices)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@callback
|
||||
def async_add_devices_discovery(hass, discovery_info, add_devices):
|
||||
"""Set up binary sensors for KNX platform configured via xknx.yaml."""
|
||||
entities = []
|
||||
for device_name in discovery_info[ATTR_DISCOVER_DEVICES]:
|
||||
device = hass.data[DATA_KNX].xknx.devices[device_name]
|
||||
entities.append(KNXBinarySensor(hass, device))
|
||||
add_devices(entities)
|
||||
|
||||
|
||||
@callback
|
||||
def async_add_devices_config(hass, config, add_devices):
|
||||
"""Set up binary senor for KNX platform configured within plattform."""
|
||||
name = config.get(CONF_NAME)
|
||||
import xknx
|
||||
binary_sensor = xknx.devices.BinarySensor(
|
||||
hass.data[DATA_KNX].xknx,
|
||||
name=name,
|
||||
group_address=config.get(CONF_ADDRESS),
|
||||
device_class=config.get(CONF_DEVICE_CLASS),
|
||||
significant_bit=config.get(CONF_SIGNIFICANT_BIT))
|
||||
hass.data[DATA_KNX].xknx.devices.add(binary_sensor)
|
||||
|
||||
entity = KNXBinarySensor(hass, binary_sensor)
|
||||
automations = config.get(CONF_AUTOMATION)
|
||||
if automations is not None:
|
||||
for automation in automations:
|
||||
counter = automation.get(CONF_COUNTER)
|
||||
hook = automation.get(CONF_HOOK)
|
||||
action = automation.get(CONF_ACTION)
|
||||
entity.automations.append(KNXAutomation(
|
||||
hass=hass, device=binary_sensor, hook=hook,
|
||||
action=action, counter=counter))
|
||||
add_devices([entity])
|
||||
|
||||
|
||||
class KNXBinarySensor(BinarySensorDevice):
|
||||
"""Representation of a KNX binary sensor."""
|
||||
|
||||
def __init__(self, hass, device):
|
||||
"""Initialization of KNXBinarySensor."""
|
||||
self.device = device
|
||||
self.hass = hass
|
||||
self.async_register_callbacks()
|
||||
self.automations = []
|
||||
|
||||
@callback
|
||||
def async_register_callbacks(self):
|
||||
"""Register callbacks to update hass after device was changed."""
|
||||
@asyncio.coroutine
|
||||
def after_update_callback(device):
|
||||
"""Callback after device was updated."""
|
||||
# pylint: disable=unused-argument
|
||||
yield from self.async_update_ha_state()
|
||||
self.device.register_device_updated_cb(after_update_callback)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the KNX device."""
|
||||
return self.device.name
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed within KNX."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor."""
|
||||
return self.device.device_class
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if the binary sensor is on."""
|
||||
return self.device.is_on()
|
||||
|
|
|
@ -103,7 +103,8 @@ class RingBinarySensor(BinarySensorDevice):
|
|||
self._data.check_alerts()
|
||||
|
||||
if self._data.alert:
|
||||
self._state = (self._sensor_type ==
|
||||
self._data.alert.get('kind'))
|
||||
if self._sensor_type == self._data.alert.get('kind') and \
|
||||
self._data.account_id == self._data.alert.get('doorbot_id'):
|
||||
self._state = True
|
||||
else:
|
||||
self._state = False
|
||||
|
|
|
@ -19,16 +19,24 @@ from homeassistant.const import (
|
|||
from homeassistant.exceptions import TemplateError
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import async_generate_entity_id
|
||||
from homeassistant.helpers.event import async_track_state_change
|
||||
from homeassistant.helpers.event import (
|
||||
async_track_state_change, async_track_same_state)
|
||||
from homeassistant.helpers.restore_state import async_get_last_state
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_DELAY_ON = 'delay_on'
|
||||
CONF_DELAY_OFF = 'delay_off'
|
||||
|
||||
SENSOR_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_VALUE_TEMPLATE): cv.template,
|
||||
vol.Optional(ATTR_FRIENDLY_NAME): cv.string,
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_DELAY_ON):
|
||||
vol.All(cv.time_period, cv.positive_timedelta),
|
||||
vol.Optional(CONF_DELAY_OFF):
|
||||
vol.All(cv.time_period, cv.positive_timedelta),
|
||||
})
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
|
@ -47,6 +55,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
|||
value_template.extract_entities())
|
||||
friendly_name = device_config.get(ATTR_FRIENDLY_NAME, device)
|
||||
device_class = device_config.get(CONF_DEVICE_CLASS)
|
||||
delay_on = device_config.get(CONF_DELAY_ON)
|
||||
delay_off = device_config.get(CONF_DELAY_OFF)
|
||||
|
||||
if value_template is not None:
|
||||
value_template.hass = hass
|
||||
|
@ -54,13 +64,13 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
|||
sensors.append(
|
||||
BinarySensorTemplate(
|
||||
hass, device, friendly_name, device_class, value_template,
|
||||
entity_ids)
|
||||
entity_ids, delay_on, delay_off)
|
||||
)
|
||||
if not sensors:
|
||||
_LOGGER.error("No sensors added")
|
||||
return False
|
||||
|
||||
async_add_devices(sensors, True)
|
||||
async_add_devices(sensors)
|
||||
return True
|
||||
|
||||
|
||||
|
@ -68,7 +78,7 @@ class BinarySensorTemplate(BinarySensorDevice):
|
|||
"""A virtual binary sensor that triggers from another sensor."""
|
||||
|
||||
def __init__(self, hass, device, friendly_name, device_class,
|
||||
value_template, entity_ids):
|
||||
value_template, entity_ids, delay_on, delay_off):
|
||||
"""Initialize the Template binary sensor."""
|
||||
self.hass = hass
|
||||
self.entity_id = async_generate_entity_id(
|
||||
|
@ -78,6 +88,8 @@ class BinarySensorTemplate(BinarySensorDevice):
|
|||
self._template = value_template
|
||||
self._state = None
|
||||
self._entities = entity_ids
|
||||
self._delay_on = delay_on
|
||||
self._delay_off = delay_off
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
|
@ -89,7 +101,7 @@ class BinarySensorTemplate(BinarySensorDevice):
|
|||
@callback
|
||||
def template_bsensor_state_listener(entity, old_state, new_state):
|
||||
"""Handle the target device state changes."""
|
||||
self.hass.async_add_job(self.async_update_ha_state(True))
|
||||
self.async_check_state()
|
||||
|
||||
@callback
|
||||
def template_bsensor_startup(event):
|
||||
|
@ -97,7 +109,7 @@ class BinarySensorTemplate(BinarySensorDevice):
|
|||
async_track_state_change(
|
||||
self.hass, self._entities, template_bsensor_state_listener)
|
||||
|
||||
self.hass.async_add_job(self.async_update_ha_state(True))
|
||||
self.hass.async_add_job(self.async_check_state)
|
||||
|
||||
self.hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_START, template_bsensor_startup)
|
||||
|
@ -122,11 +134,11 @@ class BinarySensorTemplate(BinarySensorDevice):
|
|||
"""No polling needed."""
|
||||
return False
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
"""Update the state from the template."""
|
||||
@callback
|
||||
def _async_render(self, *args):
|
||||
"""Get the state of template."""
|
||||
try:
|
||||
self._state = self._template.async_render().lower() == 'true'
|
||||
return self._template.async_render().lower() == 'true'
|
||||
except TemplateError as ex:
|
||||
if ex.args and ex.args[0].startswith(
|
||||
"UndefinedError: 'None' has no attribute"):
|
||||
|
@ -135,4 +147,29 @@ class BinarySensorTemplate(BinarySensorDevice):
|
|||
"the state is unknown", self._name)
|
||||
return
|
||||
_LOGGER.error("Could not render template %s: %s", self._name, ex)
|
||||
self._state = False
|
||||
|
||||
@callback
|
||||
def async_check_state(self):
|
||||
"""Update the state from the template."""
|
||||
state = self._async_render()
|
||||
|
||||
# return if the state don't change or is invalid
|
||||
if state is None or state == self.state:
|
||||
return
|
||||
|
||||
@callback
|
||||
def set_state():
|
||||
"""Set state of template binary sensor."""
|
||||
self._state = state
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
|
||||
# state without delay
|
||||
if (state and not self._delay_on) or \
|
||||
(not state and not self._delay_off):
|
||||
set_state()
|
||||
return
|
||||
|
||||
period = self._delay_on if state else self._delay_off
|
||||
async_track_same_state(
|
||||
self.hass, state, period, set_state, entity_ids=self._entities,
|
||||
async_check_func=self._async_render)
|
||||
|
|
57
homeassistant/components/binary_sensor/tesla.py
Normal file
57
homeassistant/components/binary_sensor/tesla.py
Normal file
|
@ -0,0 +1,57 @@
|
|||
"""
|
||||
Support for Tesla binary sensor.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/binary_sensor.tesla/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDevice, ENTITY_ID_FORMAT)
|
||||
from homeassistant.components.tesla import DOMAIN as TESLA_DOMAIN, TeslaDevice
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['tesla']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Tesla binary sensor."""
|
||||
devices = [
|
||||
TeslaBinarySensor(
|
||||
device, hass.data[TESLA_DOMAIN]['controller'], 'connectivity')
|
||||
for device in hass.data[TESLA_DOMAIN]['devices']['binary_sensor']]
|
||||
add_devices(devices, True)
|
||||
|
||||
|
||||
class TeslaBinarySensor(TeslaDevice, BinarySensorDevice):
|
||||
"""Implement an Tesla binary sensor for parking and charger."""
|
||||
|
||||
def __init__(self, tesla_device, controller, sensor_type):
|
||||
"""Initialisation of binary sensor."""
|
||||
super().__init__(tesla_device, controller)
|
||||
self._name = self.tesla_device.name
|
||||
self._state = False
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(self.tesla_id)
|
||||
self._sensor_type = sensor_type
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this binary sensor."""
|
||||
return self._sensor_type
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the binary sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return the state of the binary sensor."""
|
||||
return self._state
|
||||
|
||||
def update(self):
|
||||
"""Update the state of the device."""
|
||||
_LOGGER.debug("Updating sensor: %s", self._name)
|
||||
self.tesla_device.update()
|
||||
self._state = self.tesla_device.get_value()
|
|
@ -31,6 +31,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
devices.append(XiaomiDoorSensor(device, gateway))
|
||||
elif model == 'sensor_magnet.aq2':
|
||||
devices.append(XiaomiDoorSensor(device, gateway))
|
||||
elif model == 'sensor_wleak.aq1':
|
||||
devices.append(XiaomiWaterLeakSensor(device, gateway))
|
||||
elif model == 'smoke':
|
||||
devices.append(XiaomiSmokeSensor(device, gateway))
|
||||
elif model == 'natgas':
|
||||
|
@ -214,6 +216,35 @@ class XiaomiDoorSensor(XiaomiBinarySensor):
|
|||
return False
|
||||
|
||||
|
||||
class XiaomiWaterLeakSensor(XiaomiBinarySensor):
|
||||
"""Representation of a XiaomiWaterLeakSensor."""
|
||||
|
||||
def __init__(self, device, xiaomi_hub):
|
||||
"""Initialize the XiaomiWaterLeakSensor."""
|
||||
XiaomiBinarySensor.__init__(self, device, 'Water Leak Sensor',
|
||||
xiaomi_hub, 'status', 'moisture')
|
||||
|
||||
def parse_data(self, data):
|
||||
"""Parse data sent by gateway."""
|
||||
self._should_poll = False
|
||||
|
||||
value = data.get(self._data_key)
|
||||
if value is None:
|
||||
return False
|
||||
|
||||
if value == 'leak':
|
||||
self._should_poll = True
|
||||
if self._state:
|
||||
return False
|
||||
self._state = True
|
||||
return True
|
||||
elif value == 'no_leak':
|
||||
if self._state:
|
||||
self._state = False
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class XiaomiSmokeSensor(XiaomiBinarySensor):
|
||||
"""Representation of a XiaomiSmokeSensor."""
|
||||
|
||||
|
|
|
@ -47,8 +47,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
|
||||
devices = []
|
||||
for conf in discovery_info[ATTR_DISCOVER_DEVICES]:
|
||||
new_device = HMThermostat(hass, conf)
|
||||
new_device.link_homematic()
|
||||
new_device = HMThermostat(conf)
|
||||
devices.append(new_device)
|
||||
|
||||
add_devices(devices)
|
||||
|
|
|
@ -196,6 +196,11 @@ class RoundThermostat(ClimateDevice):
|
|||
if val['id'] == self._id:
|
||||
data = val
|
||||
|
||||
except KeyError:
|
||||
_LOGGER.error("Update failed from Honeywell server")
|
||||
self.client.user_data = None
|
||||
return
|
||||
|
||||
except StopIteration:
|
||||
_LOGGER.error("Did not receive any temperature data from the "
|
||||
"evohomeclient API")
|
||||
|
|
|
@ -1,68 +1,136 @@
|
|||
"""
|
||||
Support for KNX thermostats.
|
||||
Support for KNX/IP climate devices.
|
||||
|
||||
For more details about this platform, please refer to the documentation
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/climate.knx/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import asyncio
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.climate import (ClimateDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.components.knx import (KNXConfig, KNXMultiAddressDevice)
|
||||
from homeassistant.const import (CONF_NAME, TEMP_CELSIUS, ATTR_TEMPERATURE)
|
||||
from homeassistant.components.knx import DATA_KNX, ATTR_DISCOVER_DEVICES
|
||||
from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice
|
||||
from homeassistant.const import CONF_NAME, TEMP_CELSIUS, ATTR_TEMPERATURE
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_ADDRESS = 'address'
|
||||
CONF_SETPOINT_ADDRESS = 'setpoint_address'
|
||||
CONF_TEMPERATURE_ADDRESS = 'temperature_address'
|
||||
CONF_TARGET_TEMPERATURE_ADDRESS = 'target_temperature_address'
|
||||
CONF_OPERATION_MODE_ADDRESS = 'operation_mode_address'
|
||||
CONF_OPERATION_MODE_STATE_ADDRESS = 'operation_mode_state_address'
|
||||
CONF_CONTROLLER_STATUS_ADDRESS = 'controller_status_address'
|
||||
CONF_CONTROLLER_STATUS_STATE_ADDRESS = 'controller_status_state_address'
|
||||
CONF_OPERATION_MODE_FROST_PROTECTION_ADDRESS = \
|
||||
'operation_mode_frost_protection_address'
|
||||
CONF_OPERATION_MODE_NIGHT_ADDRESS = 'operation_mode_night_address'
|
||||
CONF_OPERATION_MODE_COMFORT_ADDRESS = 'operation_mode_comfort_address'
|
||||
|
||||
DEFAULT_NAME = 'KNX Thermostat'
|
||||
DEFAULT_NAME = 'KNX Climate'
|
||||
DEPENDENCIES = ['knx']
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_ADDRESS): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Required(CONF_SETPOINT_ADDRESS): cv.string,
|
||||
vol.Required(CONF_TEMPERATURE_ADDRESS): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Required(CONF_TARGET_TEMPERATURE_ADDRESS): cv.string,
|
||||
vol.Optional(CONF_OPERATION_MODE_ADDRESS): cv.string,
|
||||
vol.Optional(CONF_OPERATION_MODE_STATE_ADDRESS): cv.string,
|
||||
vol.Optional(CONF_CONTROLLER_STATUS_ADDRESS): cv.string,
|
||||
vol.Optional(CONF_CONTROLLER_STATUS_STATE_ADDRESS): cv.string,
|
||||
vol.Optional(CONF_OPERATION_MODE_FROST_PROTECTION_ADDRESS): cv.string,
|
||||
vol.Optional(CONF_OPERATION_MODE_NIGHT_ADDRESS): cv.string,
|
||||
vol.Optional(CONF_OPERATION_MODE_COMFORT_ADDRESS): cv.string,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Create and add an entity based on the configuration."""
|
||||
add_devices([KNXThermostat(hass, KNXConfig(config))])
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, add_devices,
|
||||
discovery_info=None):
|
||||
"""Set up climate(s) for KNX platform."""
|
||||
if DATA_KNX not in hass.data \
|
||||
or not hass.data[DATA_KNX].initialized:
|
||||
return False
|
||||
|
||||
if discovery_info is not None:
|
||||
async_add_devices_discovery(hass, discovery_info, add_devices)
|
||||
else:
|
||||
async_add_devices_config(hass, config, add_devices)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class KNXThermostat(KNXMultiAddressDevice, ClimateDevice):
|
||||
"""Representation of a KNX thermostat.
|
||||
@callback
|
||||
def async_add_devices_discovery(hass, discovery_info, add_devices):
|
||||
"""Set up climates for KNX platform configured within plattform."""
|
||||
entities = []
|
||||
for device_name in discovery_info[ATTR_DISCOVER_DEVICES]:
|
||||
device = hass.data[DATA_KNX].xknx.devices[device_name]
|
||||
entities.append(KNXClimate(hass, device))
|
||||
add_devices(entities)
|
||||
|
||||
A KNX thermostat will has the following parameters:
|
||||
- temperature (current temperature)
|
||||
- setpoint (target temperature in HASS terms)
|
||||
- operation mode selection (comfort/night/frost protection)
|
||||
|
||||
This version supports only polling. Messages from the KNX bus do not
|
||||
automatically update the state of the thermostat (to be implemented
|
||||
in future releases)
|
||||
"""
|
||||
@callback
|
||||
def async_add_devices_config(hass, config, add_devices):
|
||||
"""Set up climate for KNX platform configured within plattform."""
|
||||
import xknx
|
||||
climate = xknx.devices.Climate(
|
||||
hass.data[DATA_KNX].xknx,
|
||||
name=config.get(CONF_NAME),
|
||||
group_address_temperature=config.get(
|
||||
CONF_TEMPERATURE_ADDRESS),
|
||||
group_address_target_temperature=config.get(
|
||||
CONF_TARGET_TEMPERATURE_ADDRESS),
|
||||
group_address_setpoint=config.get(
|
||||
CONF_SETPOINT_ADDRESS),
|
||||
group_address_operation_mode=config.get(
|
||||
CONF_OPERATION_MODE_ADDRESS),
|
||||
group_address_operation_mode_state=config.get(
|
||||
CONF_OPERATION_MODE_STATE_ADDRESS),
|
||||
group_address_controller_status=config.get(
|
||||
CONF_CONTROLLER_STATUS_ADDRESS),
|
||||
group_address_controller_status_state=config.get(
|
||||
CONF_CONTROLLER_STATUS_STATE_ADDRESS),
|
||||
group_address_operation_mode_protection=config.get(
|
||||
CONF_OPERATION_MODE_FROST_PROTECTION_ADDRESS),
|
||||
group_address_operation_mode_night=config.get(
|
||||
CONF_OPERATION_MODE_NIGHT_ADDRESS),
|
||||
group_address_operation_mode_comfort=config.get(
|
||||
CONF_OPERATION_MODE_COMFORT_ADDRESS))
|
||||
hass.data[DATA_KNX].xknx.devices.add(climate)
|
||||
add_devices([KNXClimate(hass, climate)])
|
||||
|
||||
def __init__(self, hass, config):
|
||||
"""Initialize the thermostat based on the given configuration."""
|
||||
KNXMultiAddressDevice.__init__(
|
||||
self, hass, config, ['temperature', 'setpoint'], ['mode'])
|
||||
|
||||
self._unit_of_measurement = TEMP_CELSIUS # KNX always used celsius
|
||||
class KNXClimate(ClimateDevice):
|
||||
"""Representation of a KNX climate."""
|
||||
|
||||
def __init__(self, hass, device):
|
||||
"""Initialization of KNXClimate."""
|
||||
self.device = device
|
||||
self.hass = hass
|
||||
self.async_register_callbacks()
|
||||
|
||||
self._unit_of_measurement = TEMP_CELSIUS
|
||||
self._away = False # not yet supported
|
||||
self._is_fan_on = False # not yet supported
|
||||
self._current_temp = None
|
||||
self._target_temp = None
|
||||
|
||||
def async_register_callbacks(self):
|
||||
"""Register callbacks to update hass after device was changed."""
|
||||
@asyncio.coroutine
|
||||
def after_update_callback(device):
|
||||
"""Callback after device was updated."""
|
||||
# pylint: disable=unused-argument
|
||||
yield from self.async_update_ha_state()
|
||||
self.device.register_device_updated_cb(after_update_callback)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the KNX device."""
|
||||
return self.device.name
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return the polling state, is needed for the KNX thermostat."""
|
||||
return True
|
||||
"""No polling needed within KNX."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
|
@ -72,32 +140,42 @@ class KNXThermostat(KNXMultiAddressDevice, ClimateDevice):
|
|||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
return self._current_temp
|
||||
return self.device.temperature
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
return self._target_temp
|
||||
if self.device.supports_target_temperature:
|
||||
return self.device.target_temperature
|
||||
return None
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
@asyncio.coroutine
|
||||
def async_set_temperature(self, **kwargs):
|
||||
"""Set new target temperature."""
|
||||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
if temperature is None:
|
||||
return
|
||||
from knxip.conversion import float_to_knx2
|
||||
if self.device.supports_target_temperature:
|
||||
yield from self.device.set_target_temperature(temperature)
|
||||
|
||||
self.set_value('setpoint', float_to_knx2(temperature))
|
||||
_LOGGER.debug("Set target temperature to %s", temperature)
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return current operation ie. heat, cool, idle."""
|
||||
if self.device.supports_operation_mode:
|
||||
return self.device.operation_mode.value
|
||||
return None
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
@property
|
||||
def operation_list(self):
|
||||
"""Return the list of available operation modes."""
|
||||
return [operation_mode.value for
|
||||
operation_mode in
|
||||
self.device.get_supported_operation_modes()]
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_operation_mode(self, operation_mode):
|
||||
"""Set operation mode."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def update(self):
|
||||
"""Update KNX climate."""
|
||||
from knxip.conversion import knx2_to_float
|
||||
|
||||
super().update()
|
||||
|
||||
self._current_temp = knx2_to_float(self.value('temperature'))
|
||||
self._target_temp = knx2_to_float(self.value('setpoint'))
|
||||
if self.device.supports_operation_mode:
|
||||
from xknx.knx import HVACOperationMode
|
||||
knx_operation_mode = HVACOperationMode(operation_mode)
|
||||
yield from self.device.set_operation_mode(knx_operation_mode)
|
||||
|
|
93
homeassistant/components/climate/tesla.py
Normal file
93
homeassistant/components/climate/tesla.py
Normal file
|
@ -0,0 +1,93 @@
|
|||
"""
|
||||
Support for Tesla HVAC system.
|
||||
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/climate.tesla/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.const import STATE_ON, STATE_OFF
|
||||
from homeassistant.components.climate import ClimateDevice, ENTITY_ID_FORMAT
|
||||
from homeassistant.components.tesla import DOMAIN as TESLA_DOMAIN, TeslaDevice
|
||||
from homeassistant.const import (
|
||||
TEMP_FAHRENHEIT, TEMP_CELSIUS, ATTR_TEMPERATURE)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['tesla']
|
||||
|
||||
OPERATION_LIST = [STATE_ON, STATE_OFF]
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Tesla climate platform."""
|
||||
devices = [TeslaThermostat(device, hass.data[TESLA_DOMAIN]['controller'])
|
||||
for device in hass.data[TESLA_DOMAIN]['devices']['climate']]
|
||||
add_devices(devices, True)
|
||||
|
||||
|
||||
class TeslaThermostat(TeslaDevice, ClimateDevice):
|
||||
"""Representation of a Tesla climate."""
|
||||
|
||||
def __init__(self, tesla_device, controller):
|
||||
"""Initialize the Tesla device."""
|
||||
super().__init__(tesla_device, controller)
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(self.tesla_id)
|
||||
self._target_temperature = None
|
||||
self._temperature = None
|
||||
self._name = self.tesla_device.name
|
||||
|
||||
@property
|
||||
def current_operation(self):
|
||||
"""Return current operation ie. On or Off."""
|
||||
mode = self.tesla_device.is_hvac_enabled()
|
||||
if mode:
|
||||
return OPERATION_LIST[0] # On
|
||||
else:
|
||||
return OPERATION_LIST[1] # Off
|
||||
|
||||
@property
|
||||
def operation_list(self):
|
||||
"""List of available operation modes."""
|
||||
return OPERATION_LIST
|
||||
|
||||
def update(self):
|
||||
"""Called by the Tesla device callback to update state."""
|
||||
_LOGGER.debug("Updating: %s", self._name)
|
||||
self.tesla_device.update()
|
||||
self._target_temperature = self.tesla_device.get_goal_temp()
|
||||
self._temperature = self.tesla_device.get_current_temp()
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
tesla_temp_units = self.tesla_device.measurement
|
||||
|
||||
if tesla_temp_units == 'F':
|
||||
return TEMP_FAHRENHEIT
|
||||
return TEMP_CELSIUS
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
"""Return the current temperature."""
|
||||
return self._temperature
|
||||
|
||||
@property
|
||||
def target_temperature(self):
|
||||
"""Return the temperature we try to reach."""
|
||||
return self._target_temperature
|
||||
|
||||
def set_temperature(self, **kwargs):
|
||||
"""Set new target temperatures."""
|
||||
_LOGGER.debug("Setting temperature for: %s", self._name)
|
||||
temperature = kwargs.get(ATTR_TEMPERATURE)
|
||||
if temperature:
|
||||
self.tesla_device.set_temperature(temperature)
|
||||
|
||||
def set_operation_mode(self, operation_mode):
|
||||
"""Set HVAC mode (auto, cool, heat, off)."""
|
||||
_LOGGER.debug("Setting mode for: %s", self._name)
|
||||
if operation_mode == OPERATION_LIST[1]: # off
|
||||
self.tesla_device.set_status(False)
|
||||
elif operation_mode == OPERATION_LIST[0]: # heat
|
||||
self.tesla_device.set_status(True)
|
49
homeassistant/components/cloud/__init__.py
Normal file
49
homeassistant/components/cloud/__init__.py
Normal file
|
@ -0,0 +1,49 @@
|
|||
"""Component to integrate the Home Assistant cloud."""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from . import http_api, cloud_api
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
DEPENDENCIES = ['http']
|
||||
CONF_MODE = 'mode'
|
||||
MODE_DEV = 'development'
|
||||
MODE_STAGING = 'staging'
|
||||
MODE_PRODUCTION = 'production'
|
||||
DEFAULT_MODE = MODE_DEV
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Optional(CONF_MODE, default=DEFAULT_MODE):
|
||||
vol.In([MODE_DEV, MODE_STAGING, MODE_PRODUCTION]),
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Initialize the Home Assistant cloud."""
|
||||
mode = MODE_PRODUCTION
|
||||
|
||||
if DOMAIN in config:
|
||||
mode = config[DOMAIN].get(CONF_MODE)
|
||||
|
||||
if mode != 'development':
|
||||
_LOGGER.error('Only development mode is currently allowed.')
|
||||
return False
|
||||
|
||||
data = hass.data[DOMAIN] = {
|
||||
'mode': mode
|
||||
}
|
||||
|
||||
cloud = yield from cloud_api.async_load_auth(hass)
|
||||
|
||||
if cloud is not None:
|
||||
data['cloud'] = cloud
|
||||
|
||||
yield from http_api.async_setup(hass)
|
||||
return True
|
297
homeassistant/components/cloud/cloud_api.py
Normal file
297
homeassistant/components/cloud/cloud_api.py
Normal file
|
@ -0,0 +1,297 @@
|
|||
"""Package to offer tools to communicate with the cloud."""
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from urllib.parse import urljoin
|
||||
|
||||
import aiohttp
|
||||
import async_timeout
|
||||
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
from .const import AUTH_FILE, REQUEST_TIMEOUT, SERVERS
|
||||
from .util import get_mode
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
URL_CREATE_TOKEN = 'o/token/'
|
||||
URL_REVOKE_TOKEN = 'o/revoke_token/'
|
||||
URL_ACCOUNT = 'account.json'
|
||||
|
||||
|
||||
class CloudError(Exception):
|
||||
"""Base class for cloud related errors."""
|
||||
|
||||
def __init__(self, reason=None, status=None):
|
||||
"""Initialize a cloud error."""
|
||||
super().__init__(reason)
|
||||
self.status = status
|
||||
|
||||
|
||||
class Unauthenticated(CloudError):
|
||||
"""Raised when authentication failed."""
|
||||
|
||||
|
||||
class UnknownError(CloudError):
|
||||
"""Raised when an unknown error occurred."""
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_load_auth(hass):
|
||||
"""Load authentication from disk and verify it."""
|
||||
auth = yield from hass.async_add_job(_read_auth, hass)
|
||||
|
||||
if not auth:
|
||||
return None
|
||||
|
||||
cloud = Cloud(hass, auth)
|
||||
|
||||
try:
|
||||
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
||||
auth_check = yield from cloud.async_refresh_account_info()
|
||||
|
||||
if not auth_check:
|
||||
_LOGGER.error('Unable to validate credentials.')
|
||||
return None
|
||||
|
||||
return cloud
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
_LOGGER.error('Unable to reach server to validate credentials.')
|
||||
return None
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_login(hass, username, password, scope=None):
|
||||
"""Get a token using a username and password.
|
||||
|
||||
Returns a coroutine.
|
||||
"""
|
||||
data = {
|
||||
'grant_type': 'password',
|
||||
'username': username,
|
||||
'password': password
|
||||
}
|
||||
if scope is not None:
|
||||
data['scope'] = scope
|
||||
|
||||
auth = yield from _async_get_token(hass, data)
|
||||
|
||||
yield from hass.async_add_job(_write_auth, hass, auth)
|
||||
|
||||
return Cloud(hass, auth)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def _async_get_token(hass, data):
|
||||
"""Get a new token and return it as a dictionary.
|
||||
|
||||
Raises exceptions when errors occur:
|
||||
- Unauthenticated
|
||||
- UnknownError
|
||||
"""
|
||||
session = async_get_clientsession(hass)
|
||||
auth = aiohttp.BasicAuth(*_client_credentials(hass))
|
||||
|
||||
try:
|
||||
req = yield from session.post(
|
||||
_url(hass, URL_CREATE_TOKEN),
|
||||
data=data,
|
||||
auth=auth
|
||||
)
|
||||
|
||||
if req.status == 401:
|
||||
_LOGGER.error('Cloud login failed: %d', req.status)
|
||||
raise Unauthenticated(status=req.status)
|
||||
elif req.status != 200:
|
||||
_LOGGER.error('Cloud login failed: %d', req.status)
|
||||
raise UnknownError(status=req.status)
|
||||
|
||||
response = yield from req.json()
|
||||
response['expires_at'] = \
|
||||
(utcnow() + timedelta(seconds=response['expires_in'])).isoformat()
|
||||
|
||||
return response
|
||||
|
||||
except aiohttp.ClientError:
|
||||
raise UnknownError()
|
||||
|
||||
|
||||
class Cloud:
|
||||
"""Store Hass Cloud info."""
|
||||
|
||||
def __init__(self, hass, auth):
|
||||
"""Initialize Hass cloud info object."""
|
||||
self.hass = hass
|
||||
self.auth = auth
|
||||
self.account = None
|
||||
|
||||
@property
|
||||
def access_token(self):
|
||||
"""Return access token."""
|
||||
return self.auth['access_token']
|
||||
|
||||
@property
|
||||
def refresh_token(self):
|
||||
"""Get refresh token."""
|
||||
return self.auth['refresh_token']
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_refresh_account_info(self):
|
||||
"""Refresh the account info."""
|
||||
req = yield from self.async_request('get', URL_ACCOUNT)
|
||||
|
||||
if req.status != 200:
|
||||
return False
|
||||
|
||||
self.account = yield from req.json()
|
||||
return True
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_refresh_access_token(self):
|
||||
"""Get a token using a refresh token."""
|
||||
try:
|
||||
self.auth = yield from _async_get_token(self.hass, {
|
||||
'grant_type': 'refresh_token',
|
||||
'refresh_token': self.refresh_token,
|
||||
})
|
||||
|
||||
yield from self.hass.async_add_job(
|
||||
_write_auth, self.hass, self.auth)
|
||||
|
||||
return True
|
||||
except CloudError:
|
||||
return False
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_revoke_access_token(self):
|
||||
"""Revoke active access token."""
|
||||
session = async_get_clientsession(self.hass)
|
||||
client_id, client_secret = _client_credentials(self.hass)
|
||||
data = {
|
||||
'token': self.access_token,
|
||||
'client_id': client_id,
|
||||
'client_secret': client_secret
|
||||
}
|
||||
try:
|
||||
req = yield from session.post(
|
||||
_url(self.hass, URL_REVOKE_TOKEN),
|
||||
data=data,
|
||||
)
|
||||
|
||||
if req.status != 200:
|
||||
_LOGGER.error('Cloud logout failed: %d', req.status)
|
||||
raise UnknownError(status=req.status)
|
||||
|
||||
self.auth = None
|
||||
yield from self.hass.async_add_job(
|
||||
_write_auth, self.hass, None)
|
||||
|
||||
except aiohttp.ClientError:
|
||||
raise UnknownError()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_request(self, method, path, **kwargs):
|
||||
"""Make a request to Home Assistant cloud.
|
||||
|
||||
Will refresh the token if necessary.
|
||||
"""
|
||||
session = async_get_clientsession(self.hass)
|
||||
url = _url(self.hass, path)
|
||||
|
||||
if 'headers' not in kwargs:
|
||||
kwargs['headers'] = {}
|
||||
|
||||
kwargs['headers']['authorization'] = \
|
||||
'Bearer {}'.format(self.access_token)
|
||||
|
||||
request = yield from session.request(method, url, **kwargs)
|
||||
|
||||
if request.status != 403:
|
||||
return request
|
||||
|
||||
# Maybe token expired. Try refreshing it.
|
||||
reauth = yield from self.async_refresh_access_token()
|
||||
|
||||
if not reauth:
|
||||
return request
|
||||
|
||||
# Release old connection back to the pool.
|
||||
yield from request.release()
|
||||
|
||||
kwargs['headers']['authorization'] = \
|
||||
'Bearer {}'.format(self.access_token)
|
||||
|
||||
# If we are not already fetching the account info,
|
||||
# refresh the account info.
|
||||
|
||||
if path != URL_ACCOUNT:
|
||||
yield from self.async_refresh_account_info()
|
||||
|
||||
request = yield from session.request(method, url, **kwargs)
|
||||
|
||||
return request
|
||||
|
||||
|
||||
def _read_auth(hass):
|
||||
"""Read auth file."""
|
||||
path = hass.config.path(AUTH_FILE)
|
||||
|
||||
if not os.path.isfile(path):
|
||||
return None
|
||||
|
||||
with open(path) as file:
|
||||
return json.load(file).get(get_mode(hass))
|
||||
|
||||
|
||||
def _write_auth(hass, data):
|
||||
"""Write auth info for specified mode.
|
||||
|
||||
Pass in None for data to remove authentication for that mode.
|
||||
"""
|
||||
path = hass.config.path(AUTH_FILE)
|
||||
mode = get_mode(hass)
|
||||
|
||||
if os.path.isfile(path):
|
||||
with open(path) as file:
|
||||
content = json.load(file)
|
||||
else:
|
||||
content = {}
|
||||
|
||||
if data is None:
|
||||
content.pop(mode, None)
|
||||
else:
|
||||
content[mode] = data
|
||||
|
||||
with open(path, 'wt') as file:
|
||||
file.write(json.dumps(content, indent=4, sort_keys=True))
|
||||
|
||||
|
||||
def _client_credentials(hass):
|
||||
"""Get the client credentials.
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
mode = get_mode(hass)
|
||||
|
||||
if mode not in SERVERS:
|
||||
raise ValueError('Mode {} is not supported.'.format(mode))
|
||||
|
||||
return SERVERS[mode]['client_id'], SERVERS[mode]['client_secret']
|
||||
|
||||
|
||||
def _url(hass, path):
|
||||
"""Generate a url for the cloud.
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
mode = get_mode(hass)
|
||||
|
||||
if mode not in SERVERS:
|
||||
raise ValueError('Mode {} is not supported.'.format(mode))
|
||||
|
||||
return urljoin(SERVERS[mode]['host'], path)
|
14
homeassistant/components/cloud/const.py
Normal file
14
homeassistant/components/cloud/const.py
Normal file
|
@ -0,0 +1,14 @@
|
|||
"""Constants for the cloud component."""
|
||||
DOMAIN = 'cloud'
|
||||
REQUEST_TIMEOUT = 10
|
||||
AUTH_FILE = '.cloud'
|
||||
|
||||
SERVERS = {
|
||||
'development': {
|
||||
'host': 'http://localhost:8000',
|
||||
'client_id': 'HBhQxeV8H4aFBcs7jrZUeeDud0FjGEJJSZ9G6gNu',
|
||||
'client_secret': ('V1qw2NhB32cSAlP7DOezjgWNgn7ZKgq0jvVZoYSI0KCmg9rg7q4'
|
||||
'BSzoebnQnX6tuHCJiZjm2479mZmmtf2LOUdnSqOqkSpjc3js7Wu'
|
||||
'VBJrRyfgTVd43kbrEQtuOiaUpK')
|
||||
}
|
||||
}
|
119
homeassistant/components/cloud/http_api.py
Normal file
119
homeassistant/components/cloud/http_api.py
Normal file
|
@ -0,0 +1,119 @@
|
|||
"""The HTTP api to control the cloud integration."""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
import async_timeout
|
||||
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
|
||||
from . import cloud_api
|
||||
from .const import DOMAIN, REQUEST_TIMEOUT
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass):
|
||||
"""Initialize the HTTP api."""
|
||||
hass.http.register_view(CloudLoginView)
|
||||
hass.http.register_view(CloudLogoutView)
|
||||
hass.http.register_view(CloudAccountView)
|
||||
|
||||
|
||||
class CloudLoginView(HomeAssistantView):
|
||||
"""Login to Home Assistant cloud."""
|
||||
|
||||
url = '/api/cloud/login'
|
||||
name = 'api:cloud:login'
|
||||
schema = vol.Schema({
|
||||
vol.Required('username'): str,
|
||||
vol.Required('password'): str,
|
||||
})
|
||||
|
||||
@asyncio.coroutine
|
||||
def post(self, request):
|
||||
"""Validate config and return results."""
|
||||
try:
|
||||
data = yield from request.json()
|
||||
except ValueError:
|
||||
_LOGGER.error('Login with invalid JSON')
|
||||
return self.json_message('Invalid JSON.', 400)
|
||||
|
||||
try:
|
||||
self.schema(data)
|
||||
except vol.Invalid as err:
|
||||
_LOGGER.error('Login with invalid formatted data')
|
||||
return self.json_message(
|
||||
'Message format incorrect: {}'.format(err), 400)
|
||||
|
||||
hass = request.app['hass']
|
||||
phase = 1
|
||||
try:
|
||||
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
||||
cloud = yield from cloud_api.async_login(
|
||||
hass, data['username'], data['password'])
|
||||
|
||||
phase += 1
|
||||
|
||||
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
||||
yield from cloud.async_refresh_account_info()
|
||||
|
||||
except cloud_api.Unauthenticated:
|
||||
return self.json_message(
|
||||
'Authentication failed (phase {}).'.format(phase), 401)
|
||||
except cloud_api.UnknownError:
|
||||
return self.json_message(
|
||||
'Unknown error occurred (phase {}).'.format(phase), 500)
|
||||
except asyncio.TimeoutError:
|
||||
return self.json_message(
|
||||
'Unable to reach Home Assistant cloud '
|
||||
'(phase {}).'.format(phase), 502)
|
||||
|
||||
hass.data[DOMAIN]['cloud'] = cloud
|
||||
return self.json(cloud.account)
|
||||
|
||||
|
||||
class CloudLogoutView(HomeAssistantView):
|
||||
"""Log out of the Home Assistant cloud."""
|
||||
|
||||
url = '/api/cloud/logout'
|
||||
name = 'api:cloud:logout'
|
||||
|
||||
@asyncio.coroutine
|
||||
def post(self, request):
|
||||
"""Validate config and return results."""
|
||||
hass = request.app['hass']
|
||||
try:
|
||||
with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop):
|
||||
yield from \
|
||||
hass.data[DOMAIN]['cloud'].async_revoke_access_token()
|
||||
|
||||
hass.data[DOMAIN].pop('cloud')
|
||||
|
||||
return self.json({
|
||||
'result': 'ok',
|
||||
})
|
||||
except asyncio.TimeoutError:
|
||||
return self.json_message("Could not reach the server.", 502)
|
||||
except cloud_api.UnknownError as err:
|
||||
return self.json_message(
|
||||
"Error communicating with the server ({}).".format(err.status),
|
||||
502)
|
||||
|
||||
|
||||
class CloudAccountView(HomeAssistantView):
|
||||
"""Log out of the Home Assistant cloud."""
|
||||
|
||||
url = '/api/cloud/account'
|
||||
name = 'api:cloud:account'
|
||||
|
||||
@asyncio.coroutine
|
||||
def get(self, request):
|
||||
"""Validate config and return results."""
|
||||
hass = request.app['hass']
|
||||
|
||||
if 'cloud' not in hass.data[DOMAIN]:
|
||||
return self.json_message('Not logged in', 400)
|
||||
|
||||
return self.json(hass.data[DOMAIN]['cloud'].account)
|
10
homeassistant/components/cloud/util.py
Normal file
10
homeassistant/components/cloud/util.py
Normal file
|
@ -0,0 +1,10 @@
|
|||
"""Utilities for the cloud integration."""
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
def get_mode(hass):
|
||||
"""Return the current mode of the cloud component.
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
return hass.data[DOMAIN]['mode']
|
|
@ -14,7 +14,7 @@ from homeassistant.util.yaml import load_yaml, dump
|
|||
|
||||
DOMAIN = 'config'
|
||||
DEPENDENCIES = ['http']
|
||||
SECTIONS = ('core', 'group', 'hassbian', 'automation', 'script')
|
||||
SECTIONS = ('core', 'customize', 'group', 'hassbian', 'automation', 'script')
|
||||
ON_DEMAND = ('zwave')
|
||||
|
||||
|
||||
|
@ -77,11 +77,11 @@ class BaseEditConfigView(HomeAssistantView):
|
|||
"""Empty config if file not found."""
|
||||
raise NotImplementedError
|
||||
|
||||
def _get_value(self, data, config_key):
|
||||
def _get_value(self, hass, data, config_key):
|
||||
"""Get value."""
|
||||
raise NotImplementedError
|
||||
|
||||
def _write_value(self, data, config_key, new_value):
|
||||
def _write_value(self, hass, data, config_key, new_value):
|
||||
"""Set value."""
|
||||
raise NotImplementedError
|
||||
|
||||
|
@ -90,7 +90,7 @@ class BaseEditConfigView(HomeAssistantView):
|
|||
"""Fetch device specific config."""
|
||||
hass = request.app['hass']
|
||||
current = yield from self.read_config(hass)
|
||||
value = self._get_value(current, config_key)
|
||||
value = self._get_value(hass, current, config_key)
|
||||
|
||||
if value is None:
|
||||
return self.json_message('Resource not found', 404)
|
||||
|
@ -121,7 +121,7 @@ class BaseEditConfigView(HomeAssistantView):
|
|||
path = hass.config.path(self.path)
|
||||
|
||||
current = yield from self.read_config(hass)
|
||||
self._write_value(current, config_key, data)
|
||||
self._write_value(hass, current, config_key, data)
|
||||
|
||||
yield from hass.async_add_job(_write, path, current)
|
||||
|
||||
|
@ -149,11 +149,11 @@ class EditKeyBasedConfigView(BaseEditConfigView):
|
|||
"""Return an empty config."""
|
||||
return {}
|
||||
|
||||
def _get_value(self, data, config_key):
|
||||
def _get_value(self, hass, data, config_key):
|
||||
"""Get value."""
|
||||
return data.get(config_key, {})
|
||||
|
||||
def _write_value(self, data, config_key, new_value):
|
||||
def _write_value(self, hass, data, config_key, new_value):
|
||||
"""Set value."""
|
||||
data.setdefault(config_key, {}).update(new_value)
|
||||
|
||||
|
@ -165,14 +165,14 @@ class EditIdBasedConfigView(BaseEditConfigView):
|
|||
"""Return an empty config."""
|
||||
return []
|
||||
|
||||
def _get_value(self, data, config_key):
|
||||
def _get_value(self, hass, data, config_key):
|
||||
"""Get value."""
|
||||
return next(
|
||||
(val for val in data if val.get(CONF_ID) == config_key), None)
|
||||
|
||||
def _write_value(self, data, config_key, new_value):
|
||||
def _write_value(self, hass, data, config_key, new_value):
|
||||
"""Set value."""
|
||||
value = self._get_value(data, config_key)
|
||||
value = self._get_value(hass, data, config_key)
|
||||
|
||||
if value is None:
|
||||
value = {CONF_ID: config_key}
|
||||
|
|
39
homeassistant/components/config/customize.py
Normal file
39
homeassistant/components/config/customize.py
Normal file
|
@ -0,0 +1,39 @@
|
|||
"""Provide configuration end points for Customize."""
|
||||
import asyncio
|
||||
|
||||
from homeassistant.components.config import EditKeyBasedConfigView
|
||||
from homeassistant.components import async_reload_core_config
|
||||
from homeassistant.config import DATA_CUSTOMIZE
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
CONFIG_PATH = 'customize.yaml'
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass):
|
||||
"""Set up the Customize config API."""
|
||||
hass.http.register_view(CustomizeConfigView(
|
||||
'customize', 'config', CONFIG_PATH, cv.entity_id, dict,
|
||||
post_write_hook=async_reload_core_config
|
||||
))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class CustomizeConfigView(EditKeyBasedConfigView):
|
||||
"""Configure a list of entries."""
|
||||
|
||||
def _get_value(self, hass, data, config_key):
|
||||
"""Get value."""
|
||||
customize = hass.data.get(DATA_CUSTOMIZE, {}).get(config_key) or {}
|
||||
return {'global': customize, 'local': data.get(config_key, {})}
|
||||
|
||||
def _write_value(self, hass, data, config_key, new_value):
|
||||
"""Set value."""
|
||||
data[config_key] = new_value
|
||||
|
||||
state = hass.states.get(config_key)
|
||||
state_attributes = dict(state.attributes)
|
||||
state_attributes.update(new_value)
|
||||
hass.states.async_set(config_key, state.state, state_attributes)
|
220
homeassistant/components/counter.py
Normal file
220
homeassistant/components/counter.py
Normal file
|
@ -0,0 +1,220 @@
|
|||
"""
|
||||
Component to count within automations.
|
||||
|
||||
For more details about this component, please refer to the documentation
|
||||
at https://home-assistant.io/components/counter/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.const import (ATTR_ENTITY_ID, CONF_ICON, CONF_NAME)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.restore_state import async_get_last_state
|
||||
from homeassistant.loader import bind_hass
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_INITIAL = 'initial'
|
||||
ATTR_STEP = 'step'
|
||||
|
||||
CONF_INITIAL = 'initial'
|
||||
CONF_STEP = 'step'
|
||||
|
||||
DEFAULT_INITIAL = 0
|
||||
DEFAULT_STEP = 1
|
||||
DOMAIN = 'counter'
|
||||
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
SERVICE_DECREMENT = 'decrement'
|
||||
SERVICE_INCREMENT = 'increment'
|
||||
SERVICE_RESET = 'reset'
|
||||
|
||||
SERVICE_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
})
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
cv.slug: vol.Any({
|
||||
vol.Optional(CONF_ICON): cv.icon,
|
||||
vol.Optional(CONF_INITIAL, default=DEFAULT_INITIAL):
|
||||
cv.positive_int,
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_STEP, default=DEFAULT_STEP): cv.positive_int,
|
||||
}, None)
|
||||
})
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def increment(hass, entity_id):
|
||||
"""Increment a counter."""
|
||||
hass.add_job(async_increment, hass, entity_id)
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def async_increment(hass, entity_id):
|
||||
"""Increment a counter."""
|
||||
hass.async_add_job(hass.services.async_call(
|
||||
DOMAIN, SERVICE_INCREMENT, {ATTR_ENTITY_ID: entity_id}))
|
||||
|
||||
|
||||
@bind_hass
|
||||
def decrement(hass, entity_id):
|
||||
"""Decrement a counter."""
|
||||
hass.add_job(async_decrement, hass, entity_id)
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def async_decrement(hass, entity_id):
|
||||
"""Decrement a counter."""
|
||||
hass.async_add_job(hass.services.async_call(
|
||||
DOMAIN, SERVICE_DECREMENT, {ATTR_ENTITY_ID: entity_id}))
|
||||
|
||||
|
||||
@bind_hass
|
||||
def reset(hass, entity_id):
|
||||
"""Reset a counter."""
|
||||
hass.add_job(async_reset, hass, entity_id)
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def async_reset(hass, entity_id):
|
||||
"""Reset a counter."""
|
||||
hass.async_add_job(hass.services.async_call(
|
||||
DOMAIN, SERVICE_RESET, {ATTR_ENTITY_ID: entity_id}))
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Set up a counter."""
|
||||
component = EntityComponent(_LOGGER, DOMAIN, hass)
|
||||
|
||||
entities = []
|
||||
|
||||
for object_id, cfg in config[DOMAIN].items():
|
||||
if not cfg:
|
||||
cfg = {}
|
||||
|
||||
name = cfg.get(CONF_NAME)
|
||||
initial = cfg.get(CONF_INITIAL)
|
||||
step = cfg.get(CONF_STEP)
|
||||
icon = cfg.get(CONF_ICON)
|
||||
|
||||
entities.append(Counter(object_id, name, initial, step, icon))
|
||||
|
||||
if not entities:
|
||||
return False
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_handler_service(service):
|
||||
"""Handle a call to the counter services."""
|
||||
target_counters = component.async_extract_from_service(service)
|
||||
|
||||
if service.service == SERVICE_INCREMENT:
|
||||
attr = 'async_increment'
|
||||
elif service.service == SERVICE_DECREMENT:
|
||||
attr = 'async_decrement'
|
||||
elif service.service == SERVICE_RESET:
|
||||
attr = 'async_reset'
|
||||
|
||||
tasks = [getattr(counter, attr)() for counter in target_counters]
|
||||
if tasks:
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
descriptions = yield from hass.async_add_job(
|
||||
load_yaml_config_file, os.path.join(
|
||||
os.path.dirname(__file__), 'services.yaml')
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_INCREMENT, async_handler_service,
|
||||
descriptions[DOMAIN][SERVICE_INCREMENT], SERVICE_SCHEMA)
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_DECREMENT, async_handler_service,
|
||||
descriptions[DOMAIN][SERVICE_DECREMENT], SERVICE_SCHEMA)
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_RESET, async_handler_service,
|
||||
descriptions[DOMAIN][SERVICE_RESET], SERVICE_SCHEMA)
|
||||
|
||||
yield from component.async_add_entities(entities)
|
||||
return True
|
||||
|
||||
|
||||
class Counter(Entity):
|
||||
"""Representation of a counter."""
|
||||
|
||||
def __init__(self, object_id, name, initial, step, icon):
|
||||
"""Initialize a counter."""
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(object_id)
|
||||
self._name = name
|
||||
self._step = step
|
||||
self._state = self._initial = initial
|
||||
self._icon = icon
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""If entity should be polled."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return name of the counter."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon to be used for this entity."""
|
||||
return self._icon
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the current value of the counter."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return {
|
||||
ATTR_INITIAL: self._initial,
|
||||
ATTR_STEP: self._step,
|
||||
}
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Call when entity about to be added to Home Assistant."""
|
||||
# If not None, we got an initial value.
|
||||
if self._state is not None:
|
||||
return
|
||||
|
||||
state = yield from async_get_last_state(self.hass, self.entity_id)
|
||||
self._state = state and state.state == state
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_decrement(self):
|
||||
"""Decrement the counter."""
|
||||
self._state -= self._step
|
||||
yield from self.async_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_increment(self):
|
||||
"""Increment a counter."""
|
||||
self._state += self._step
|
||||
yield from self.async_update_ha_state()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_reset(self):
|
||||
"""Reset a counter."""
|
||||
self._state = self._initial
|
||||
yield from self.async_update_ha_state()
|
49
homeassistant/components/cover/abode.py
Normal file
49
homeassistant/components/cover/abode.py
Normal file
|
@ -0,0 +1,49 @@
|
|||
"""
|
||||
This component provides HA cover support for Abode Security System.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/cover.abode/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.abode import AbodeDevice, DATA_ABODE
|
||||
from homeassistant.components.cover import CoverDevice
|
||||
|
||||
|
||||
DEPENDENCIES = ['abode']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up Abode cover devices."""
|
||||
import abodepy.helpers.constants as CONST
|
||||
|
||||
abode = hass.data[DATA_ABODE]
|
||||
|
||||
sensors = []
|
||||
for sensor in abode.get_devices(type_filter=(CONST.DEVICE_SECURE_BARRIER)):
|
||||
sensors.append(AbodeCover(abode, sensor))
|
||||
|
||||
add_devices(sensors)
|
||||
|
||||
|
||||
class AbodeCover(AbodeDevice, CoverDevice):
|
||||
"""Representation of an Abode cover."""
|
||||
|
||||
def __init__(self, controller, device):
|
||||
"""Initialize the Abode device."""
|
||||
AbodeDevice.__init__(self, controller, device)
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return true if cover is closed, else False."""
|
||||
return self._device.is_open is False
|
||||
|
||||
def close_cover(self):
|
||||
"""Issue close command to cover."""
|
||||
self._device.close_cover()
|
||||
|
||||
def open_cover(self):
|
||||
"""Issue open command to cover."""
|
||||
self._device.open_cover()
|
|
@ -21,8 +21,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
|
||||
devices = []
|
||||
for conf in discovery_info[ATTR_DISCOVER_DEVICES]:
|
||||
new_device = HMCover(hass, conf)
|
||||
new_device.link_homematic()
|
||||
new_device = HMCover(conf)
|
||||
devices.append(new_device)
|
||||
|
||||
add_devices(devices)
|
||||
|
|
|
@ -1,185 +1,239 @@
|
|||
"""
|
||||
Support for KNX covers.
|
||||
Support for KNX/IP covers.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/cover.knx/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import asyncio
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.knx import DATA_KNX, ATTR_DISCOVER_DEVICES
|
||||
from homeassistant.helpers.event import async_track_utc_time_change
|
||||
from homeassistant.components.cover import (
|
||||
CoverDevice, PLATFORM_SCHEMA, ATTR_POSITION, DEVICE_CLASSES_SCHEMA,
|
||||
SUPPORT_OPEN, SUPPORT_CLOSE, SUPPORT_SET_POSITION, SUPPORT_STOP,
|
||||
SUPPORT_SET_TILT_POSITION
|
||||
)
|
||||
from homeassistant.components.knx import (KNXConfig, KNXMultiAddressDevice)
|
||||
from homeassistant.const import (CONF_NAME, CONF_DEVICE_CLASS)
|
||||
CoverDevice, PLATFORM_SCHEMA, SUPPORT_OPEN, SUPPORT_CLOSE,
|
||||
SUPPORT_SET_POSITION, SUPPORT_STOP, SUPPORT_SET_TILT_POSITION,
|
||||
ATTR_POSITION, ATTR_TILT_POSITION)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import CONF_NAME
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_GETPOSITION_ADDRESS = 'getposition_address'
|
||||
CONF_SETPOSITION_ADDRESS = 'setposition_address'
|
||||
CONF_GETANGLE_ADDRESS = 'getangle_address'
|
||||
CONF_SETANGLE_ADDRESS = 'setangle_address'
|
||||
CONF_STOP = 'stop_address'
|
||||
CONF_UPDOWN = 'updown_address'
|
||||
CONF_MOVE_LONG_ADDRESS = 'move_long_address'
|
||||
CONF_MOVE_SHORT_ADDRESS = 'move_short_address'
|
||||
CONF_POSITION_ADDRESS = 'position_address'
|
||||
CONF_POSITION_STATE_ADDRESS = 'position_state_address'
|
||||
CONF_ANGLE_ADDRESS = 'angle_address'
|
||||
CONF_ANGLE_STATE_ADDRESS = 'angle_state_address'
|
||||
CONF_TRAVELLING_TIME_DOWN = 'travelling_time_down'
|
||||
CONF_TRAVELLING_TIME_UP = 'travelling_time_up'
|
||||
CONF_INVERT_POSITION = 'invert_position'
|
||||
CONF_INVERT_ANGLE = 'invert_angle'
|
||||
|
||||
DEFAULT_TRAVEL_TIME = 25
|
||||
DEFAULT_NAME = 'KNX Cover'
|
||||
DEPENDENCIES = ['knx']
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_UPDOWN): cv.string,
|
||||
vol.Required(CONF_STOP): cv.string,
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_GETPOSITION_ADDRESS): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_SETPOSITION_ADDRESS): cv.string,
|
||||
vol.Optional(CONF_MOVE_LONG_ADDRESS): cv.string,
|
||||
vol.Optional(CONF_MOVE_SHORT_ADDRESS): cv.string,
|
||||
vol.Optional(CONF_POSITION_ADDRESS): cv.string,
|
||||
vol.Optional(CONF_POSITION_STATE_ADDRESS): cv.string,
|
||||
vol.Optional(CONF_ANGLE_ADDRESS): cv.string,
|
||||
vol.Optional(CONF_ANGLE_STATE_ADDRESS): cv.string,
|
||||
vol.Optional(CONF_TRAVELLING_TIME_DOWN, default=DEFAULT_TRAVEL_TIME):
|
||||
cv.positive_int,
|
||||
vol.Optional(CONF_TRAVELLING_TIME_UP, default=DEFAULT_TRAVEL_TIME):
|
||||
cv.positive_int,
|
||||
vol.Optional(CONF_INVERT_POSITION, default=False): cv.boolean,
|
||||
vol.Inclusive(CONF_GETANGLE_ADDRESS, 'angle'): cv.string,
|
||||
vol.Inclusive(CONF_SETANGLE_ADDRESS, 'angle'): cv.string,
|
||||
vol.Optional(CONF_INVERT_ANGLE, default=False): cv.boolean,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Create and add an entity based on the configuration."""
|
||||
add_devices([KNXCover(hass, KNXConfig(config))])
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, add_devices,
|
||||
discovery_info=None):
|
||||
"""Set up cover(s) for KNX platform."""
|
||||
if DATA_KNX not in hass.data \
|
||||
or not hass.data[DATA_KNX].initialized:
|
||||
return False
|
||||
|
||||
if discovery_info is not None:
|
||||
async_add_devices_discovery(hass, discovery_info, add_devices)
|
||||
else:
|
||||
async_add_devices_config(hass, config, add_devices)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class KNXCover(KNXMultiAddressDevice, CoverDevice):
|
||||
"""Representation of a KNX cover. e.g. a rollershutter."""
|
||||
@callback
|
||||
def async_add_devices_discovery(hass, discovery_info, add_devices):
|
||||
"""Set up covers for KNX platform configured via xknx.yaml."""
|
||||
entities = []
|
||||
for device_name in discovery_info[ATTR_DISCOVER_DEVICES]:
|
||||
device = hass.data[DATA_KNX].xknx.devices[device_name]
|
||||
entities.append(KNXCover(hass, device))
|
||||
add_devices(entities)
|
||||
|
||||
def __init__(self, hass, config):
|
||||
|
||||
@callback
|
||||
def async_add_devices_config(hass, config, add_devices):
|
||||
"""Set up cover for KNX platform configured within plattform."""
|
||||
import xknx
|
||||
cover = xknx.devices.Cover(
|
||||
hass.data[DATA_KNX].xknx,
|
||||
name=config.get(CONF_NAME),
|
||||
group_address_long=config.get(CONF_MOVE_LONG_ADDRESS),
|
||||
group_address_short=config.get(CONF_MOVE_SHORT_ADDRESS),
|
||||
group_address_position_state=config.get(
|
||||
CONF_POSITION_STATE_ADDRESS),
|
||||
group_address_angle=config.get(CONF_ANGLE_ADDRESS),
|
||||
group_address_angle_state=config.get(CONF_ANGLE_STATE_ADDRESS),
|
||||
group_address_position=config.get(CONF_POSITION_ADDRESS),
|
||||
travel_time_down=config.get(CONF_TRAVELLING_TIME_DOWN),
|
||||
travel_time_up=config.get(CONF_TRAVELLING_TIME_UP))
|
||||
|
||||
invert_position = config.get(CONF_INVERT_POSITION)
|
||||
invert_angle = config.get(CONF_INVERT_ANGLE)
|
||||
hass.data[DATA_KNX].xknx.devices.add(cover)
|
||||
add_devices([KNXCover(hass, cover, invert_position, invert_angle)])
|
||||
|
||||
|
||||
class KNXCover(CoverDevice):
|
||||
"""Representation of a KNX cover."""
|
||||
|
||||
def __init__(self, hass, device, invert_position=False,
|
||||
invert_angle=False):
|
||||
"""Initialize the cover."""
|
||||
KNXMultiAddressDevice.__init__(
|
||||
self, hass, config,
|
||||
['updown', 'stop'], # required
|
||||
optional=['setposition', 'getposition',
|
||||
'getangle', 'setangle']
|
||||
)
|
||||
self._device_class = config.config.get(CONF_DEVICE_CLASS)
|
||||
self._invert_position = config.config.get(CONF_INVERT_POSITION)
|
||||
self._invert_angle = config.config.get(CONF_INVERT_ANGLE)
|
||||
self._hass = hass
|
||||
self._current_pos = None
|
||||
self._target_pos = None
|
||||
self._current_tilt = None
|
||||
self._target_tilt = None
|
||||
self._supported_features = SUPPORT_OPEN | SUPPORT_CLOSE | \
|
||||
SUPPORT_SET_POSITION | SUPPORT_STOP
|
||||
self.device = device
|
||||
self.invert_position = invert_position
|
||||
self.invert_angle = invert_angle
|
||||
self.hass = hass
|
||||
self.async_register_callbacks()
|
||||
|
||||
# Tilt is only supported, if there is a angle get and set address
|
||||
if CONF_SETANGLE_ADDRESS in config.config:
|
||||
_LOGGER.debug("%s: Tilt supported at addresses %s, %s",
|
||||
self.name, config.config.get(CONF_SETANGLE_ADDRESS),
|
||||
config.config.get(CONF_GETANGLE_ADDRESS))
|
||||
self._supported_features = self._supported_features | \
|
||||
SUPPORT_SET_TILT_POSITION
|
||||
self._unsubscribe_auto_updater = None
|
||||
|
||||
@callback
|
||||
def async_register_callbacks(self):
|
||||
"""Register callbacks to update hass after device was changed."""
|
||||
@asyncio.coroutine
|
||||
def after_update_callback(device):
|
||||
"""Callback after device was updated."""
|
||||
# pylint: disable=unused-argument
|
||||
yield from self.async_update_ha_state()
|
||||
self.device.register_device_updated_cb(after_update_callback)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the KNX device."""
|
||||
return self.device.name
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Polling is needed for the KNX cover."""
|
||||
return True
|
||||
"""No polling needed within KNX."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
return self._supported_features
|
||||
supported_features = SUPPORT_OPEN | SUPPORT_CLOSE | \
|
||||
SUPPORT_SET_POSITION | SUPPORT_STOP
|
||||
if self.device.supports_angle:
|
||||
supported_features |= SUPPORT_SET_TILT_POSITION
|
||||
return supported_features
|
||||
|
||||
@property
|
||||
def current_cover_position(self):
|
||||
"""Return the current position of the cover."""
|
||||
return int(self.from_knx_position(
|
||||
self.device.current_position(),
|
||||
self.invert_position))
|
||||
|
||||
@property
|
||||
def is_closed(self):
|
||||
"""Return if the cover is closed."""
|
||||
if self.current_cover_position is not None:
|
||||
if self.current_cover_position > 0:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
return self.device.is_closed()
|
||||
|
||||
@property
|
||||
def current_cover_position(self):
|
||||
"""Return current position of cover.
|
||||
@asyncio.coroutine
|
||||
def async_close_cover(self, **kwargs):
|
||||
"""Close the cover."""
|
||||
if not self.device.is_closed():
|
||||
yield from self.device.set_down()
|
||||
self.start_auto_updater()
|
||||
|
||||
None is unknown, 0 is closed, 100 is fully open.
|
||||
"""
|
||||
return self._current_pos
|
||||
@asyncio.coroutine
|
||||
def async_open_cover(self, **kwargs):
|
||||
"""Open the cover."""
|
||||
if not self.device.is_open():
|
||||
yield from self.device.set_up()
|
||||
self.start_auto_updater()
|
||||
|
||||
@property
|
||||
def target_position(self):
|
||||
"""Return the position we are trying to reach: 0 - 100."""
|
||||
return self._target_pos
|
||||
@asyncio.coroutine
|
||||
def async_set_cover_position(self, **kwargs):
|
||||
"""Move the cover to a specific position."""
|
||||
if ATTR_POSITION in kwargs:
|
||||
position = kwargs[ATTR_POSITION]
|
||||
knx_position = self.to_knx_position(position, self.invert_position)
|
||||
yield from self.device.set_position(knx_position)
|
||||
self.start_auto_updater()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_stop_cover(self, **kwargs):
|
||||
"""Stop the cover."""
|
||||
yield from self.device.stop()
|
||||
self.stop_auto_updater()
|
||||
|
||||
@property
|
||||
def current_cover_tilt_position(self):
|
||||
"""Return current position of cover.
|
||||
"""Return current tilt position of cover."""
|
||||
if not self.device.supports_angle:
|
||||
return None
|
||||
return int(self.from_knx_position(
|
||||
self.device.angle,
|
||||
self.invert_angle))
|
||||
|
||||
None is unknown, 0 is closed, 100 is fully open.
|
||||
"""
|
||||
return self._current_tilt
|
||||
@asyncio.coroutine
|
||||
def async_set_cover_tilt_position(self, **kwargs):
|
||||
"""Move the cover tilt to a specific position."""
|
||||
if ATTR_TILT_POSITION in kwargs:
|
||||
position = kwargs[ATTR_TILT_POSITION]
|
||||
knx_position = self.to_knx_position(position, self.invert_angle)
|
||||
yield from self.device.set_angle(knx_position)
|
||||
|
||||
@property
|
||||
def target_tilt(self):
|
||||
"""Return the tilt angle (in %) we are trying to reach: 0 - 100."""
|
||||
return self._target_tilt
|
||||
def start_auto_updater(self):
|
||||
"""Start the autoupdater to update HASS while cover is moving."""
|
||||
if self._unsubscribe_auto_updater is None:
|
||||
self._unsubscribe_auto_updater = async_track_utc_time_change(
|
||||
self.hass, self.auto_updater_hook)
|
||||
|
||||
def set_cover_position(self, **kwargs):
|
||||
"""Set new target position."""
|
||||
position = kwargs.get(ATTR_POSITION)
|
||||
if position is None:
|
||||
return
|
||||
def stop_auto_updater(self):
|
||||
"""Stop the autoupdater."""
|
||||
if self._unsubscribe_auto_updater is not None:
|
||||
self._unsubscribe_auto_updater()
|
||||
self._unsubscribe_auto_updater = None
|
||||
|
||||
if self._invert_position:
|
||||
position = 100-position
|
||||
@callback
|
||||
def auto_updater_hook(self, now):
|
||||
"""Callback for autoupdater."""
|
||||
# pylint: disable=unused-argument
|
||||
self.hass.async_add_job(self.async_update_ha_state())
|
||||
if self.device.position_reached():
|
||||
self.stop_auto_updater()
|
||||
|
||||
self._target_pos = position
|
||||
self.set_percentage('setposition', position)
|
||||
_LOGGER.debug("%s: Set target position to %d", self.name, position)
|
||||
self.hass.add_job(self.device.auto_stop_if_necessary())
|
||||
|
||||
def update(self):
|
||||
"""Update device state."""
|
||||
super().update()
|
||||
value = self.get_percentage('getposition')
|
||||
if value is not None:
|
||||
self._current_pos = value
|
||||
if self._invert_position:
|
||||
self._current_pos = 100-value
|
||||
_LOGGER.debug("%s: position = %d", self.name, value)
|
||||
@staticmethod
|
||||
def from_knx_position(raw, invert):
|
||||
"""Convert KNX position [0...255] to hass position [100...0]."""
|
||||
position = round((raw/256)*100)
|
||||
if not invert:
|
||||
position = 100 - position
|
||||
return position
|
||||
|
||||
if self._supported_features & SUPPORT_SET_TILT_POSITION:
|
||||
value = self.get_percentage('getangle')
|
||||
if value is not None:
|
||||
self._current_tilt = value
|
||||
if self._invert_angle:
|
||||
self._current_tilt = 100-value
|
||||
_LOGGER.debug("%s: tilt = %d", self.name, value)
|
||||
|
||||
def open_cover(self, **kwargs):
|
||||
"""Open the cover."""
|
||||
_LOGGER.debug("%s: open: updown = 0", self.name)
|
||||
self.set_int_value('updown', 0)
|
||||
|
||||
def close_cover(self, **kwargs):
|
||||
"""Close the cover."""
|
||||
_LOGGER.debug("%s: open: updown = 1", self.name)
|
||||
self.set_int_value('updown', 1)
|
||||
|
||||
def stop_cover(self, **kwargs):
|
||||
"""Stop the cover movement."""
|
||||
_LOGGER.debug("%s: stop: stop = 1", self.name)
|
||||
self.set_int_value('stop', 1)
|
||||
|
||||
def set_cover_tilt_position(self, tilt_position, **kwargs):
|
||||
"""Move the cover til to a specific position."""
|
||||
if self._invert_angle:
|
||||
tilt_position = 100-tilt_position
|
||||
|
||||
self._target_tilt = round(tilt_position, -1)
|
||||
self.set_percentage('setangle', tilt_position)
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this device, from component DEVICE_CLASSES."""
|
||||
return self._device_class
|
||||
@staticmethod
|
||||
def to_knx_position(value, invert):
|
||||
"""Convert hass position [100...0] to KNX position [0...255]."""
|
||||
knx_position = round(value/100*255.4)
|
||||
if not invert:
|
||||
knx_position = 255-knx_position
|
||||
print(value, " -> ", knx_position)
|
||||
return knx_position
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
"""
|
||||
Support for Lutron Caseta SerenaRollerShade.
|
||||
Support for Lutron Caseta shades.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/cover.lutron_caseta/
|
||||
"""
|
||||
import logging
|
||||
|
||||
|
||||
from homeassistant.components.cover import (
|
||||
CoverDevice, SUPPORT_OPEN, SUPPORT_CLOSE, SUPPORT_SET_POSITION)
|
||||
CoverDevice, SUPPORT_OPEN, SUPPORT_CLOSE, SUPPORT_SET_POSITION,
|
||||
ATTR_POSITION, DOMAIN)
|
||||
from homeassistant.components.lutron_caseta import (
|
||||
LUTRON_CASETA_SMARTBRIDGE, LutronCasetaDevice)
|
||||
|
||||
|
@ -19,11 +19,10 @@ DEPENDENCIES = ['lutron_caseta']
|
|||
|
||||
# pylint: disable=unused-argument
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Lutron Caseta Serena shades as a cover device."""
|
||||
"""Set up the Lutron Caseta shades as a cover device."""
|
||||
devs = []
|
||||
bridge = hass.data[LUTRON_CASETA_SMARTBRIDGE]
|
||||
cover_devices = bridge.get_devices_by_types(["SerenaRollerShade",
|
||||
"SerenaHoneycombShade"])
|
||||
cover_devices = bridge.get_devices_by_domain(DOMAIN)
|
||||
for cover_device in cover_devices:
|
||||
dev = LutronCasetaCover(cover_device, bridge)
|
||||
devs.append(dev)
|
||||
|
@ -32,7 +31,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
|
||||
|
||||
class LutronCasetaCover(LutronCasetaDevice, CoverDevice):
|
||||
"""Representation of a Lutron Serena shade."""
|
||||
"""Representation of a Lutron shade."""
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
|
@ -42,24 +41,26 @@ class LutronCasetaCover(LutronCasetaDevice, CoverDevice):
|
|||
@property
|
||||
def is_closed(self):
|
||||
"""Return if the cover is closed."""
|
||||
return self._state["current_state"] < 1
|
||||
return self._state['current_state'] < 1
|
||||
|
||||
@property
|
||||
def current_cover_position(self):
|
||||
"""Return the current position of cover."""
|
||||
return self._state["current_state"]
|
||||
return self._state['current_state']
|
||||
|
||||
def close_cover(self):
|
||||
def close_cover(self, **kwargs):
|
||||
"""Close the cover."""
|
||||
self._smartbridge.set_value(self._device_id, 0)
|
||||
|
||||
def open_cover(self):
|
||||
def open_cover(self, **kwargs):
|
||||
"""Open the cover."""
|
||||
self._smartbridge.set_value(self._device_id, 100)
|
||||
|
||||
def set_cover_position(self, position, **kwargs):
|
||||
"""Move the roller shutter to a specific position."""
|
||||
self._smartbridge.set_value(self._device_id, position)
|
||||
def set_cover_position(self, **kwargs):
|
||||
"""Move the shade to a specific position."""
|
||||
if ATTR_POSITION in kwargs:
|
||||
position = kwargs[ATTR_POSITION]
|
||||
self._smartbridge.set_value(self._device_id, position)
|
||||
|
||||
def update(self):
|
||||
"""Call when forcing a refresh of the device."""
|
||||
|
|
|
@ -16,7 +16,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
|||
"""Set up the RFXtrx cover."""
|
||||
import RFXtrx as rfxtrxmod
|
||||
|
||||
covers = rfxtrx.get_devices_from_config(config, RfxtrxCover, hass)
|
||||
covers = rfxtrx.get_devices_from_config(config, RfxtrxCover)
|
||||
add_devices_callback(covers)
|
||||
|
||||
def cover_update(event):
|
||||
|
@ -26,7 +26,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
|||
not event.device.known_to_be_rollershutter:
|
||||
return
|
||||
|
||||
new_device = rfxtrx.get_new_device(event, config, RfxtrxCover, hass)
|
||||
new_device = rfxtrx.get_new_device(event, config, RfxtrxCover)
|
||||
if new_device:
|
||||
add_devices_callback([new_device])
|
||||
|
||||
|
|
|
@ -24,10 +24,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
|
||||
|
||||
class XiaomiGenericCover(XiaomiDevice, CoverDevice):
|
||||
"""Representation of a XiaomiPlug."""
|
||||
"""Representation of a XiaomiGenericCover."""
|
||||
|
||||
def __init__(self, device, name, data_key, xiaomi_hub):
|
||||
"""Initialize the XiaomiPlug."""
|
||||
"""Initialize the XiaomiGenericCover."""
|
||||
self._data_key = data_key
|
||||
self._pos = 0
|
||||
XiaomiDevice.__init__(self, device, name, xiaomi_hub)
|
||||
|
@ -44,19 +44,19 @@ class XiaomiGenericCover(XiaomiDevice, CoverDevice):
|
|||
|
||||
def close_cover(self, **kwargs):
|
||||
"""Close the cover."""
|
||||
self._write_to_hub(self._sid, self._data_key['status'], 'close')
|
||||
self._write_to_hub(self._sid, **{self._data_key['status']: 'close'})
|
||||
|
||||
def open_cover(self, **kwargs):
|
||||
"""Open the cover."""
|
||||
self._write_to_hub(self._sid, self._data_key['status'], 'open')
|
||||
self._write_to_hub(self._sid, **{self._data_key['status']: 'open'})
|
||||
|
||||
def stop_cover(self, **kwargs):
|
||||
"""Stop the cover."""
|
||||
self._write_to_hub(self._sid, self._data_key['status'], 'stop')
|
||||
self._write_to_hub(self._sid, **{self._data_key['status']: 'stop'})
|
||||
|
||||
def set_cover_position(self, position, **kwargs):
|
||||
"""Move the cover to a specific position."""
|
||||
self._write_to_hub(self._sid, self._data_key['pos'], str(position))
|
||||
self._write_to_hub(self._sid, **{self._data_key['pos']: str(position)})
|
||||
|
||||
def parse_data(self, data):
|
||||
"""Parse data sent by gateway."""
|
||||
|
|
|
@ -205,6 +205,7 @@ class AutomaticData(object):
|
|||
self.hass = hass
|
||||
self.devices = devices
|
||||
self.vehicle_info = {}
|
||||
self.vehicle_seen = {}
|
||||
self.client = client
|
||||
self.session = session
|
||||
self.async_see = async_see
|
||||
|
@ -236,6 +237,14 @@ class AutomaticData(object):
|
|||
return
|
||||
yield from self.get_vehicle_info(vehicle)
|
||||
|
||||
if event.created_at < self.vehicle_seen[event.vehicle.id]:
|
||||
# Skip events received out of order
|
||||
_LOGGER.debug("Skipping out of order event. Event Created %s. "
|
||||
"Last seen event: %s.", event.created_at,
|
||||
self.vehicle_seen[event.vehicle.id])
|
||||
return
|
||||
self.vehicle_seen[event.vehicle.id] = event.created_at
|
||||
|
||||
kwargs = self.vehicle_info[event.vehicle.id]
|
||||
if kwargs is None:
|
||||
# Ignored device
|
||||
|
@ -323,15 +332,17 @@ class AutomaticData(object):
|
|||
if self.devices is not None and name not in self.devices:
|
||||
self.vehicle_info[vehicle.id] = None
|
||||
return
|
||||
else:
|
||||
self.vehicle_info[vehicle.id] = kwargs = {
|
||||
ATTR_DEV_ID: vehicle.id,
|
||||
ATTR_HOST_NAME: name,
|
||||
ATTR_MAC: vehicle.id,
|
||||
ATTR_ATTRIBUTES: {
|
||||
ATTR_FUEL_LEVEL: vehicle.fuel_level_percent,
|
||||
}
|
||||
|
||||
self.vehicle_info[vehicle.id] = kwargs = {
|
||||
ATTR_DEV_ID: vehicle.id,
|
||||
ATTR_HOST_NAME: name,
|
||||
ATTR_MAC: vehicle.id,
|
||||
ATTR_ATTRIBUTES: {
|
||||
ATTR_FUEL_LEVEL: vehicle.fuel_level_percent,
|
||||
}
|
||||
}
|
||||
self.vehicle_seen[vehicle.id] = \
|
||||
vehicle.updated_at or vehicle.created_at
|
||||
|
||||
if vehicle.latest_location is not None:
|
||||
location = vehicle.latest_location
|
||||
|
@ -352,4 +363,7 @@ class AutomaticData(object):
|
|||
kwargs[ATTR_GPS] = (location.lat, location.lon)
|
||||
kwargs[ATTR_GPS_ACCURACY] = location.accuracy_m
|
||||
|
||||
if trips[0].ended_at >= self.vehicle_seen[vehicle.id]:
|
||||
self.vehicle_seen[vehicle.id] = trips[0].ended_at
|
||||
|
||||
return kwargs
|
||||
|
|
127
homeassistant/components/device_tracker/geofency.py
Executable file
127
homeassistant/components/device_tracker/geofency.py
Executable file
|
@ -0,0 +1,127 @@
|
|||
"""
|
||||
Support for the Geofency platform.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.geofency/
|
||||
"""
|
||||
import asyncio
|
||||
from functools import partial
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.device_tracker import PLATFORM_SCHEMA
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.const import (
|
||||
ATTR_LATITUDE, ATTR_LONGITUDE, HTTP_UNPROCESSABLE_ENTITY, STATE_NOT_HOME)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util import slugify
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['http']
|
||||
|
||||
BEACON_DEV_PREFIX = 'beacon'
|
||||
CONF_MOBILE_BEACONS = 'mobile_beacons'
|
||||
|
||||
LOCATION_ENTRY = '1'
|
||||
LOCATION_EXIT = '0'
|
||||
|
||||
URL = '/api/geofency'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_MOBILE_BEACONS): vol.All(
|
||||
cv.ensure_list, [cv.string]),
|
||||
})
|
||||
|
||||
|
||||
def setup_scanner(hass, config, see, discovery_info=None):
|
||||
"""Set up an endpoint for the Geofency application."""
|
||||
mobile_beacons = config.get(CONF_MOBILE_BEACONS) or []
|
||||
|
||||
hass.http.register_view(GeofencyView(see, mobile_beacons))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class GeofencyView(HomeAssistantView):
|
||||
"""View to handle Geofency requests."""
|
||||
|
||||
url = URL
|
||||
name = 'api:geofency'
|
||||
|
||||
def __init__(self, see, mobile_beacons):
|
||||
"""Initialize Geofency url endpoints."""
|
||||
self.see = see
|
||||
self.mobile_beacons = [slugify(beacon) for beacon in mobile_beacons]
|
||||
|
||||
@asyncio.coroutine
|
||||
def post(self, request):
|
||||
"""Handle Geofency requests."""
|
||||
data = yield from request.post()
|
||||
hass = request.app['hass']
|
||||
|
||||
data = self._validate_data(data)
|
||||
if not data:
|
||||
return ("Invalid data", HTTP_UNPROCESSABLE_ENTITY)
|
||||
|
||||
if self._is_mobile_beacon(data):
|
||||
return (yield from self._set_location(hass, data, None))
|
||||
else:
|
||||
if data['entry'] == LOCATION_ENTRY:
|
||||
location_name = data['name']
|
||||
else:
|
||||
location_name = STATE_NOT_HOME
|
||||
|
||||
return (yield from self._set_location(hass, data, location_name))
|
||||
|
||||
@staticmethod
|
||||
def _validate_data(data):
|
||||
"""Validate POST payload."""
|
||||
data = data.copy()
|
||||
|
||||
required_attributes = ['address', 'device', 'entry',
|
||||
'latitude', 'longitude', 'name']
|
||||
|
||||
valid = True
|
||||
for attribute in required_attributes:
|
||||
if attribute not in data:
|
||||
valid = False
|
||||
_LOGGER.error("'%s' not specified in message", attribute)
|
||||
|
||||
if not valid:
|
||||
return False
|
||||
|
||||
data['address'] = data['address'].replace('\n', ' ')
|
||||
data['device'] = slugify(data['device'])
|
||||
data['name'] = slugify(data['name'])
|
||||
|
||||
data[ATTR_LATITUDE] = float(data[ATTR_LATITUDE])
|
||||
data[ATTR_LONGITUDE] = float(data[ATTR_LONGITUDE])
|
||||
|
||||
return data
|
||||
|
||||
def _is_mobile_beacon(self, data):
|
||||
"""Check if we have a mobile beacon."""
|
||||
return 'beaconUUID' in data and data['name'] in self.mobile_beacons
|
||||
|
||||
@staticmethod
|
||||
def _device_name(data):
|
||||
"""Return name of device tracker."""
|
||||
if 'beaconUUID' in data:
|
||||
return "{}_{}".format(BEACON_DEV_PREFIX, data['name'])
|
||||
else:
|
||||
return data['device']
|
||||
|
||||
@asyncio.coroutine
|
||||
def _set_location(self, hass, data, location_name):
|
||||
"""Fire HA event to set location."""
|
||||
device = self._device_name(data)
|
||||
|
||||
yield from hass.async_add_job(
|
||||
partial(self.see, dev_id=device,
|
||||
gps=(data[ATTR_LATITUDE], data[ATTR_LONGITUDE]),
|
||||
location_name=location_name,
|
||||
attributes=data))
|
||||
|
||||
return "Setting location for {}".format(device)
|
57
homeassistant/components/device_tracker/tesla.py
Normal file
57
homeassistant/components/device_tracker/tesla.py
Normal file
|
@ -0,0 +1,57 @@
|
|||
"""
|
||||
Support for the Tesla platform.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.tesla/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.tesla import DOMAIN as TESLA_DOMAIN
|
||||
from homeassistant.helpers.event import track_utc_time_change
|
||||
from homeassistant.util import slugify
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['tesla']
|
||||
|
||||
|
||||
def setup_scanner(hass, config, see, discovery_info=None):
|
||||
"""Set up the Tesla tracker."""
|
||||
TeslaDeviceTracker(
|
||||
hass, config, see,
|
||||
hass.data[TESLA_DOMAIN]['devices']['devices_tracker'])
|
||||
return True
|
||||
|
||||
|
||||
class TeslaDeviceTracker(object):
|
||||
"""A class representing a Tesla device."""
|
||||
|
||||
def __init__(self, hass, config, see, tesla_devices):
|
||||
"""Initialize the Tesla device scanner."""
|
||||
self.hass = hass
|
||||
self.see = see
|
||||
self.devices = tesla_devices
|
||||
self._update_info()
|
||||
|
||||
track_utc_time_change(
|
||||
self.hass, self._update_info, second=range(0, 60, 30))
|
||||
|
||||
def _update_info(self, now=None):
|
||||
"""Update the device info."""
|
||||
for device in self.devices:
|
||||
device.update()
|
||||
name = device.name
|
||||
_LOGGER.debug("Updating device position: %s", name)
|
||||
dev_id = slugify(device.uniq_name)
|
||||
location = device.get_location()
|
||||
lat = location['latitude']
|
||||
lon = location['longitude']
|
||||
attrs = {
|
||||
'trackr_id': dev_id,
|
||||
'id': dev_id,
|
||||
'name': name
|
||||
}
|
||||
self.see(
|
||||
dev_id=dev_id, host_name=name,
|
||||
gps=(lat, lon), attributes=attrs
|
||||
)
|
|
@ -20,11 +20,12 @@ def setup_scanner(hass, config, see, discovery_info=None):
|
|||
return
|
||||
|
||||
vin, _ = discovery_info
|
||||
vehicle = hass.data[DATA_KEY].vehicles[vin]
|
||||
voc = hass.data[DATA_KEY]
|
||||
vehicle = voc.vehicles[vin]
|
||||
|
||||
def see_vehicle(vehicle):
|
||||
"""Handle the reporting of the vehicle position."""
|
||||
host_name = vehicle.registration_number
|
||||
host_name = voc.vehicle_name(vehicle)
|
||||
dev_id = 'volvo_{}'.format(slugify(host_name))
|
||||
see(dev_id=dev_id,
|
||||
host_name=host_name,
|
||||
|
|
|
@ -100,6 +100,7 @@ def async_setup(hass, config):
|
|||
|
||||
# We do not know how to handle this service.
|
||||
if not comp_plat:
|
||||
logger.info("Unknown service discovered: %s %s", service, info)
|
||||
return
|
||||
|
||||
discovery_hash = json.dumps([service, info], sort_keys=True)
|
||||
|
|
|
@ -28,6 +28,7 @@ URL_PANEL_COMPONENT_FP = '/frontend/panels/{}-{}.html'
|
|||
STATIC_PATH = os.path.join(os.path.dirname(__file__), 'www_static/')
|
||||
|
||||
ATTR_THEMES = 'themes'
|
||||
ATTR_EXTRA_HTML_URL = 'extra_html_url'
|
||||
DEFAULT_THEME_COLOR = '#03A9F4'
|
||||
MANIFEST_JSON = {
|
||||
'background_color': '#FFFFFF',
|
||||
|
@ -50,6 +51,7 @@ for size in (192, 384, 512, 1024):
|
|||
})
|
||||
|
||||
DATA_PANELS = 'frontend_panels'
|
||||
DATA_EXTRA_HTML_URL = 'frontend_extra_html_url'
|
||||
DATA_INDEX_VIEW = 'frontend_index_view'
|
||||
DATA_THEMES = 'frontend_themes'
|
||||
DATA_DEFAULT_THEME = 'frontend_default_theme'
|
||||
|
@ -66,6 +68,8 @@ CONFIG_SCHEMA = vol.Schema({
|
|||
vol.Optional(ATTR_THEMES): vol.Schema({
|
||||
cv.string: {cv.string: cv.string}
|
||||
}),
|
||||
vol.Optional(ATTR_EXTRA_HTML_URL):
|
||||
vol.All(cv.ensure_list, [cv.string]),
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
@ -105,14 +109,13 @@ def register_panel(hass, component_name, path, md5=None, sidebar_title=None,
|
|||
|
||||
component_name: name of the web component
|
||||
path: path to the HTML of the web component
|
||||
(required unless url is provided)
|
||||
md5: the md5 hash of the web component (for versioning, optional)
|
||||
sidebar_title: title to show in the sidebar (optional)
|
||||
sidebar_icon: icon to show next to title in sidebar (optional)
|
||||
url_path: name to use in the url (defaults to component_name)
|
||||
url: for the web component (for dev environment, optional)
|
||||
url: for the web component (optional)
|
||||
config: config to be passed into the web component
|
||||
|
||||
Warning: this API will probably change. Use at own risk.
|
||||
"""
|
||||
panels = hass.data.get(DATA_PANELS)
|
||||
if panels is None:
|
||||
|
@ -123,14 +126,16 @@ def register_panel(hass, component_name, path, md5=None, sidebar_title=None,
|
|||
|
||||
if url_path in panels:
|
||||
_LOGGER.warning("Overwriting component %s", url_path)
|
||||
if not os.path.isfile(path):
|
||||
_LOGGER.error(
|
||||
"Panel %s component does not exist: %s", component_name, path)
|
||||
return
|
||||
|
||||
if md5 is None:
|
||||
with open(path) as fil:
|
||||
md5 = hashlib.md5(fil.read().encode('utf-8')).hexdigest()
|
||||
if url is None:
|
||||
if not os.path.isfile(path):
|
||||
_LOGGER.error(
|
||||
"Panel %s component does not exist: %s", component_name, path)
|
||||
return
|
||||
|
||||
if md5 is None:
|
||||
with open(path) as fil:
|
||||
md5 = hashlib.md5(fil.read().encode('utf-8')).hexdigest()
|
||||
|
||||
data = {
|
||||
'url_path': url_path,
|
||||
|
@ -169,6 +174,15 @@ def register_panel(hass, component_name, path, md5=None, sidebar_title=None,
|
|||
'get', '/{}/{{extra:.+}}'.format(url_path), index_view.get)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def add_extra_html_url(hass, url):
|
||||
"""Register extra html url to load."""
|
||||
url_set = hass.data.get(DATA_EXTRA_HTML_URL)
|
||||
if url_set is None:
|
||||
url_set = hass.data[DATA_EXTRA_HTML_URL] = set()
|
||||
url_set.add(url)
|
||||
|
||||
|
||||
def add_manifest_json_key(key, val):
|
||||
"""Add a keyval to the manifest.json."""
|
||||
MANIFEST_JSON[key] = val
|
||||
|
@ -208,6 +222,9 @@ def setup(hass, config):
|
|||
else:
|
||||
hass.data[DATA_PANELS] = {}
|
||||
|
||||
if DATA_EXTRA_HTML_URL not in hass.data:
|
||||
hass.data[DATA_EXTRA_HTML_URL] = set()
|
||||
|
||||
register_built_in_panel(hass, 'map', 'Map', 'mdi:account-location')
|
||||
|
||||
for panel in ('dev-event', 'dev-info', 'dev-service', 'dev-state',
|
||||
|
@ -217,6 +234,9 @@ def setup(hass, config):
|
|||
themes = config.get(DOMAIN, {}).get(ATTR_THEMES)
|
||||
setup_themes(hass, themes)
|
||||
|
||||
for url in config.get(DOMAIN, {}).get(ATTR_EXTRA_HTML_URL, []):
|
||||
add_extra_html_url(hass, url)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
@ -362,7 +382,9 @@ class IndexView(HomeAssistantView):
|
|||
compatibility_url=compatibility_url, no_auth=no_auth,
|
||||
icons_url=icons_url, icons=FINGERPRINTS['mdi.html'],
|
||||
panel_url=panel_url, panels=hass.data[DATA_PANELS],
|
||||
dev_mode=request.app[KEY_DEVELOPMENT])
|
||||
dev_mode=request.app[KEY_DEVELOPMENT],
|
||||
theme_color=MANIFEST_JSON['theme_color'],
|
||||
extra_urls=hass.data[DATA_EXTRA_HTML_URL])
|
||||
|
||||
return web.Response(text=resp, content_type='text/html')
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
<meta name="msapplication-TileColor" content="#3fbbf4ff"/>
|
||||
<meta name='mobile-web-app-capable' content='yes'>
|
||||
<meta name='viewport' content='width=device-width, user-scalable=no'>
|
||||
<meta name='theme-color' content='#03a9f4'>
|
||||
<meta name='theme-color' content='{{ theme_color }}'>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Roboto', 'Noto', sans-serif;
|
||||
|
@ -97,6 +97,10 @@
|
|||
<link rel='import' href='{{ panel_url }}' onerror='initError()' async>
|
||||
{% endif -%}
|
||||
<link rel='import' href='{{ icons_url }}' async>
|
||||
{% for extra_url in extra_urls -%}
|
||||
<link rel='import' href='{{ extra_url }}' async>
|
||||
{% endfor -%}
|
||||
|
||||
<script>
|
||||
var webComponentsSupported = (
|
||||
'registerElement' in document &&
|
||||
|
|
|
@ -3,22 +3,22 @@
|
|||
FINGERPRINTS = {
|
||||
"compatibility.js": "1686167ff210e001f063f5c606b2e74b",
|
||||
"core.js": "2a7d01e45187c7d4635da05065b5e54e",
|
||||
"frontend.html": "6c8192a4393c9e83516dc8177b75c23d",
|
||||
"mdi.html": "e91f61a039ed0a9936e7ee5360da3870",
|
||||
"frontend.html": "c04709d3517dd3fd34b2f7d6bba6ec8e",
|
||||
"mdi.html": "89074face5529f5fe6fbae49ecb3e88b",
|
||||
"micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a",
|
||||
"panels/ha-panel-config.html": "bd20a3b11b46522e3c705a0b6a72b9dc",
|
||||
"panels/ha-panel-config.html": "0091008947ed61a6691c28093a6a6fcd",
|
||||
"panels/ha-panel-dev-event.html": "d409e7ab537d9fe629126d122345279c",
|
||||
"panels/ha-panel-dev-info.html": "b0e55eb657fd75f21aba2426ac0cedc0",
|
||||
"panels/ha-panel-dev-mqtt.html": "94b222b013a98583842de3e72d5888c6",
|
||||
"panels/ha-panel-dev-service.html": "422b2c181ee0713fa31d45a64e605baf",
|
||||
"panels/ha-panel-dev-state.html": "7948d3dba058f31517d880df8ed0e857",
|
||||
"panels/ha-panel-dev-template.html": "f47b6910d8e4880e22cc508ca452f9b6",
|
||||
"panels/ha-panel-dev-template.html": "928e7b81b9c113b70edc9f4a1d051827",
|
||||
"panels/ha-panel-hassio.html": "b46e7619f3c355f872d5370741d89f6a",
|
||||
"panels/ha-panel-history.html": "fe2daac10a14f51fa3eb7d23978df1f7",
|
||||
"panels/ha-panel-iframe.html": "56930204d6e067a3d600cf030f4b34c8",
|
||||
"panels/ha-panel-kiosk.html": "b40aa5cb52dd7675bea744afcf9eebf8",
|
||||
"panels/ha-panel-logbook.html": "771afdcf48dc7e308b0282417d2e02d8",
|
||||
"panels/ha-panel-mailbox.html": "a8cca44ca36553e91565e3c894ea6323",
|
||||
"panels/ha-panel-map.html": "c2544fff3eedb487d44105cf94b335ec",
|
||||
"panels/ha-panel-map.html": "565db019147162080c21af962afc097f",
|
||||
"panels/ha-panel-shopping-list.html": "d8cfd0ecdb3aa6214c0f6908c34c7141"
|
||||
}
|
||||
|
|
File diff suppressed because one or more lines are too long
Binary file not shown.
|
@ -1 +1 @@
|
|||
Subproject commit 07d5d6e8a9205f77f83f68615695bcbc73cf83e3
|
||||
Subproject commit 19187ce518190823a8ccc5e7fc3d262cd218fa74
|
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
|
@ -1,2 +1,2 @@
|
|||
<html><head></head><body><dom-module id="ha-panel-dev-template"><template><style is="custom-style" include="ha-style iron-flex iron-positioning"></style><style>:host{-ms-user-select:initial;-webkit-user-select:initial;-moz-user-select:initial;}.content{padding:16px;}.edit-pane{margin-right:16px;}.edit-pane a{color:var(--dark-primary-color);}.horizontal .edit-pane{max-width:50%;}.render-pane{position:relative;max-width:50%;}.render-spinner{position:absolute;top:8px;right:8px;}.rendered{@apply (--paper-font-code1)
|
||||
clear: both;white-space:pre-wrap;}.rendered.error{color:red;}</style><app-header-layout has-scrolling-region=""><app-header slot="header" fixed=""><app-toolbar><ha-menu-button narrow="[[narrow]]" show-menu="[[showMenu]]"></ha-menu-button><div main-title="">Templates</div></app-toolbar></app-header><div class$="[[computeFormClasses(narrow)]]"><div class="edit-pane"><p>Templates are rendered using the Jinja2 template engine with some Home Assistant specific extensions.</p><ul><li><a href="http://jinja.pocoo.org/docs/dev/templates/" target="_blank">Jinja2 template documentation</a></li><li><a href="https://home-assistant.io/topics/templating/" target="_blank">Home Assistant template extensions</a></li></ul><paper-textarea label="Template" value="{{template}}"></paper-textarea></div><div class="render-pane"><paper-spinner class="render-spinner" active="[[rendering]]"></paper-spinner><pre class$="[[computeRenderedClasses(error)]]">[[processed]]</pre></div></div></app-header-layout></template></dom-module><script>Polymer({is:"ha-panel-dev-template",properties:{hass:{type:Object},narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},error:{type:Boolean,value:!1},rendering:{type:Boolean,value:!1},template:{type:String,value:'Imitate available variables:\n{% set my_test_json = {\n "temperature": 25,\n "unit": "°C"\n} %}\n\nThe temperature is {{ my_test_json.temperature }} {{ my_test_json.unit }}. \n\n{% if is_state("device_tracker.paulus", "home") and \n is_state("device_tracker.anne_therese", "home") -%}\n\n You are both home, you silly\n\n{%- else -%}\n\n Anne Therese is at {{ states("device_tracker.anne_therese") }} and Paulus is at {{ states("device_tracker.paulus") }}\n\n{%- endif %}\n\nFor loop example:\n{% for state in states.sensor -%}\n {%- if loop.first %}The {% elif loop.last %} and the {% else %}, the {% endif -%}\n {{ state.name | lower }} is {{state.state}} {{- state.attributes.unit_of_measurement}}\n{%- endfor -%}.',observer:"templateChanged"},processed:{type:String,value:""}},computeFormClasses:function(e){return e?"content fit":"content fit layout horizontal"},computeRenderedClasses:function(e){return e?"error rendered":"rendered"},templateChanged:function(){this.error&&(this.error=!1),this.debounce("render-template",this.renderTemplate.bind(this),500)},renderTemplate:function(){this.rendering=!0,this.hass.callApi("POST","template",{template:this.template}).then(function(e){this.processed=e,this.rendering=!1}.bind(this),function(e){this.processed=e.body.message,this.error=!0,this.rendering=!1}.bind(this))}});</script></body></html>
|
||||
clear: both;white-space:pre-wrap;}.rendered.error{color:red;}</style><app-header-layout has-scrolling-region=""><app-header slot="header" fixed=""><app-toolbar><ha-menu-button narrow="[[narrow]]" show-menu="[[showMenu]]"></ha-menu-button><div main-title="">Templates</div></app-toolbar></app-header><div class$="[[computeFormClasses(narrow)]]"><div class="edit-pane"><p>Templates are rendered using the Jinja2 template engine with some Home Assistant specific extensions.</p><ul><li><a href="http://jinja.pocoo.org/docs/dev/templates/" target="_blank">Jinja2 template documentation</a></li><li><a href="https://home-assistant.io/topics/templating/" target="_blank">Home Assistant template extensions</a></li></ul><paper-textarea label="Template" value="{{template}}"></paper-textarea></div><div class="render-pane"><paper-spinner class="render-spinner" active="[[rendering]]"></paper-spinner><pre class$="[[computeRenderedClasses(error)]]">[[processed]]</pre></div></div></app-header-layout></template></dom-module><script>Polymer({is:"ha-panel-dev-template",properties:{hass:{type:Object},narrow:{type:Boolean,value:!1},showMenu:{type:Boolean,value:!1},error:{type:Boolean,value:!1},rendering:{type:Boolean,value:!1},template:{type:String,value:'Imitate available variables:\n{% set my_test_json = {\n "temperature": 25,\n "unit": "°C"\n} %}\n\nThe temperature is {{ my_test_json.temperature }} {{ my_test_json.unit }}. \n\n{% if is_state("device_tracker.paulus", "home") and \n is_state("device_tracker.anne_therese", "home") -%}\n\n You are both home, you silly\n\n{%- else -%}\n\n Anne Therese is at {{ states("device_tracker.anne_therese") }} and Paulus is at {{ states("device_tracker.paulus") }}\n\n{%- endif %}\n\nFor loop example:\n{% for state in states.sensor -%}\n {%- if loop.first %}The {% elif loop.last %} and the {% else %}, the {% endif -%}\n {{ state.name | lower }} is {{state.state_with_unit}}\n{%- endfor -%}.',observer:"templateChanged"},processed:{type:String,value:""}},computeFormClasses:function(e){return e?"content fit":"content fit layout horizontal"},computeRenderedClasses:function(e){return e?"error rendered":"rendered"},templateChanged:function(){this.error&&(this.error=!1),this.debounce("render-template",this.renderTemplate.bind(this),500)},renderTemplate:function(){this.rendering=!0,this.hass.callApi("POST","template",{template:this.template}).then(function(e){this.processed=e,this.rendering=!1}.bind(this),function(e){this.processed=e.body.message,this.error=!0,this.rendering=!1}.bind(this))}});</script></body></html>
|
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
|
@ -37,7 +37,7 @@
|
|||
/* eslint-disable indent, no-unused-vars, no-multiple-empty-lines, max-nested-callbacks, space-before-function-paren, quotes, comma-spacing */
|
||||
'use strict';
|
||||
|
||||
var precacheConfig = [["/","535d629ec4d3936dba0ca4ca84dabeb2"],["/frontend/panels/dev-event-d409e7ab537d9fe629126d122345279c.html","936814991f2a5e23d61d29f0d40f81b8"],["/frontend/panels/dev-info-b0e55eb657fd75f21aba2426ac0cedc0.html","1fa953b0224470f70d4e87bbe4dff191"],["/frontend/panels/dev-mqtt-94b222b013a98583842de3e72d5888c6.html","dc3ddfac58397feda97317358f0aecbb"],["/frontend/panels/dev-service-422b2c181ee0713fa31d45a64e605baf.html","ae7d26b1c8c3309fd3c65944f89ea03f"],["/frontend/panels/dev-state-7948d3dba058f31517d880df8ed0e857.html","ff8156bb1a52490fcc07466556fce0e1"],["/frontend/panels/dev-template-f47b6910d8e4880e22cc508ca452f9b6.html","9aa0675e01373c6bc2737438bb84a9ec"],["/frontend/panels/map-c2544fff3eedb487d44105cf94b335ec.html","113c5bf9a68a74c62e50cd354034e78b"],["/static/compatibility-1686167ff210e001f063f5c606b2e74b.js","6ee7b5e2dd82b510c3bd92f7e215988e"],["/static/core-2a7d01e45187c7d4635da05065b5e54e.js","90a0a8a6a6dd0ca41b16f40e7d23924d"],["/static/frontend-6c8192a4393c9e83516dc8177b75c23d.html","56d5bfe9e11a8b81a686f20aeae3c359"],["/static/mdi-e91f61a039ed0a9936e7ee5360da3870.html","5e587bc82719b740a4f0798722a83aee"],["static/fonts/roboto/Roboto-Bold.ttf","d329cc8b34667f114a95422aaad1b063"],["static/fonts/roboto/Roboto-Light.ttf","7b5fb88f12bec8143f00e21bc3222124"],["static/fonts/roboto/Roboto-Medium.ttf","fe13e4170719c2fc586501e777bde143"],["static/fonts/roboto/Roboto-Regular.ttf","ac3f799d5bbaf5196fab15ab8de8431c"],["static/icons/favicon-192x192.png","419903b8422586a7e28021bbe9011175"],["static/icons/favicon.ico","04235bda7843ec2fceb1cbe2bc696cf4"],["static/images/card_media_player_bg.png","a34281d1c1835d338a642e90930e61aa"]];
|
||||
var precacheConfig = [["/","eceffe0debe81636e1eb8604e6eefbd6"],["/frontend/panels/dev-event-d409e7ab537d9fe629126d122345279c.html","936814991f2a5e23d61d29f0d40f81b8"],["/frontend/panels/dev-info-b0e55eb657fd75f21aba2426ac0cedc0.html","1fa953b0224470f70d4e87bbe4dff191"],["/frontend/panels/dev-mqtt-94b222b013a98583842de3e72d5888c6.html","dc3ddfac58397feda97317358f0aecbb"],["/frontend/panels/dev-service-422b2c181ee0713fa31d45a64e605baf.html","ae7d26b1c8c3309fd3c65944f89ea03f"],["/frontend/panels/dev-state-7948d3dba058f31517d880df8ed0e857.html","ff8156bb1a52490fcc07466556fce0e1"],["/frontend/panels/dev-template-928e7b81b9c113b70edc9f4a1d051827.html","312c8313800b44c83bcb8dc2df30c759"],["/frontend/panels/map-565db019147162080c21af962afc097f.html","a1a360042395682335e2f471dddad309"],["/static/compatibility-1686167ff210e001f063f5c606b2e74b.js","6ee7b5e2dd82b510c3bd92f7e215988e"],["/static/core-2a7d01e45187c7d4635da05065b5e54e.js","90a0a8a6a6dd0ca41b16f40e7d23924d"],["/static/frontend-c04709d3517dd3fd34b2f7d6bba6ec8e.html","e072f7bbe595bcb104d117a45592459d"],["/static/mdi-89074face5529f5fe6fbae49ecb3e88b.html","97754e463f9e56a95c813d4d8e792347"],["static/fonts/roboto/Roboto-Bold.ttf","d329cc8b34667f114a95422aaad1b063"],["static/fonts/roboto/Roboto-Light.ttf","7b5fb88f12bec8143f00e21bc3222124"],["static/fonts/roboto/Roboto-Medium.ttf","fe13e4170719c2fc586501e777bde143"],["static/fonts/roboto/Roboto-Regular.ttf","ac3f799d5bbaf5196fab15ab8de8431c"],["static/icons/favicon-192x192.png","419903b8422586a7e28021bbe9011175"],["static/icons/favicon.ico","04235bda7843ec2fceb1cbe2bc696cf4"],["static/images/card_media_player_bg.png","a34281d1c1835d338a642e90930e61aa"]];
|
||||
var cacheName = 'sw-precache-v3--' + (self.registration ? self.registration.scope : '');
|
||||
|
||||
|
||||
|
|
Binary file not shown.
|
@ -31,7 +31,7 @@ DOMAIN = 'hdmi_cec'
|
|||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_DISPLAY_NAME = "HomeAssistant"
|
||||
DEFAULT_DISPLAY_NAME = "HA"
|
||||
CONF_TYPES = 'types'
|
||||
|
||||
ICON_UNKNOWN = 'mdi:help'
|
||||
|
@ -181,7 +181,7 @@ def setup(hass: HomeAssistant, base_config):
|
|||
if host:
|
||||
adapter = TcpAdapter(host, name=display_name, activate_source=False)
|
||||
else:
|
||||
adapter = CecAdapter(name=display_name, activate_source=False)
|
||||
adapter = CecAdapter(name=display_name[:12], activate_source=False)
|
||||
hdmi_network = HDMINetwork(adapter, loop=loop)
|
||||
|
||||
def _volume(call):
|
||||
|
|
|
@ -4,8 +4,8 @@ Support for HomeMatic devices.
|
|||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/homematic/
|
||||
"""
|
||||
import asyncio
|
||||
import os
|
||||
import time
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from functools import partial
|
||||
|
@ -18,7 +18,7 @@ from homeassistant.const import (
|
|||
CONF_PLATFORM, CONF_HOSTS, CONF_NAME, ATTR_ENTITY_ID)
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.event import track_time_interval
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
|
||||
REQUIREMENTS = ['pyhomematic==0.1.30']
|
||||
|
@ -121,7 +121,6 @@ CONF_RESOLVENAMES_OPTIONS = [
|
|||
]
|
||||
|
||||
DATA_HOMEMATIC = 'homematic'
|
||||
DATA_DELAY = 'homematic_delay'
|
||||
DATA_DEVINIT = 'homematic_devinit'
|
||||
DATA_STORE = 'homematic_store'
|
||||
|
||||
|
@ -134,7 +133,6 @@ CONF_CALLBACK_PORT = 'callback_port'
|
|||
CONF_RESOLVENAMES = 'resolvenames'
|
||||
CONF_VARIABLES = 'variables'
|
||||
CONF_DEVICES = 'devices'
|
||||
CONF_DELAY = 'delay'
|
||||
CONF_PRIMARY = 'primary'
|
||||
|
||||
DEFAULT_LOCAL_IP = '0.0.0.0'
|
||||
|
@ -145,7 +143,6 @@ DEFAULT_USERNAME = 'Admin'
|
|||
DEFAULT_PASSWORD = ''
|
||||
DEFAULT_VARIABLES = False
|
||||
DEFAULT_DEVICES = True
|
||||
DEFAULT_DELAY = 0.5
|
||||
DEFAULT_PRIMARY = False
|
||||
|
||||
|
||||
|
@ -177,7 +174,6 @@ CONFIG_SCHEMA = vol.Schema({
|
|||
}},
|
||||
vol.Optional(CONF_LOCAL_IP, default=DEFAULT_LOCAL_IP): cv.string,
|
||||
vol.Optional(CONF_LOCAL_PORT, default=DEFAULT_LOCAL_PORT): cv.port,
|
||||
vol.Optional(CONF_DELAY, default=DEFAULT_DELAY): vol.Coerce(float),
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
@ -249,7 +245,6 @@ def setup(hass, config):
|
|||
"""Set up the Homematic component."""
|
||||
from pyhomematic import HMConnection
|
||||
|
||||
hass.data[DATA_DELAY] = config[DOMAIN].get(CONF_DELAY)
|
||||
hass.data[DATA_DEVINIT] = {}
|
||||
hass.data[DATA_STORE] = set()
|
||||
|
||||
|
@ -277,7 +272,7 @@ def setup(hass, config):
|
|||
|
||||
# Create server thread
|
||||
bound_system_callback = partial(_system_callback_handler, hass, config)
|
||||
hass.data[DATA_HOMEMATIC] = HMConnection(
|
||||
hass.data[DATA_HOMEMATIC] = homematic = HMConnection(
|
||||
local=config[DOMAIN].get(CONF_LOCAL_IP),
|
||||
localport=config[DOMAIN].get(CONF_LOCAL_PORT),
|
||||
remotes=remotes,
|
||||
|
@ -286,7 +281,7 @@ def setup(hass, config):
|
|||
)
|
||||
|
||||
# Start server thread, connect to hosts, initialize to receive events
|
||||
hass.data[DATA_HOMEMATIC].start()
|
||||
homematic.start()
|
||||
|
||||
# Stops server when HASS is shutting down
|
||||
hass.bus.listen_once(
|
||||
|
@ -296,7 +291,7 @@ def setup(hass, config):
|
|||
entity_hubs = []
|
||||
for _, hub_data in hosts.items():
|
||||
entity_hubs.append(HMHub(
|
||||
hass, hub_data[CONF_NAME], hub_data[CONF_VARIABLES]))
|
||||
homematic, hub_data[CONF_NAME], hub_data[CONF_VARIABLES]))
|
||||
|
||||
# Register HomeMatic services
|
||||
descriptions = load_yaml_config_file(
|
||||
|
@ -359,7 +354,7 @@ def setup(hass, config):
|
|||
|
||||
def _service_handle_reconnect(service):
|
||||
"""Service to reconnect all HomeMatic hubs."""
|
||||
hass.data[DATA_HOMEMATIC].reconnect()
|
||||
homematic.reconnect()
|
||||
|
||||
hass.services.register(
|
||||
DOMAIN, SERVICE_RECONNECT, _service_handle_reconnect,
|
||||
|
@ -575,24 +570,27 @@ def _device_from_servicecall(hass, service):
|
|||
class HMHub(Entity):
|
||||
"""The HomeMatic hub. (CCU2/HomeGear)."""
|
||||
|
||||
def __init__(self, hass, name, use_variables):
|
||||
def __init__(self, homematic, name, use_variables):
|
||||
"""Initialize HomeMatic hub."""
|
||||
self.hass = hass
|
||||
self.entity_id = "{}.{}".format(DOMAIN, name.lower())
|
||||
self._homematic = hass.data[DATA_HOMEMATIC]
|
||||
self._homematic = homematic
|
||||
self._variables = {}
|
||||
self._name = name
|
||||
self._state = STATE_UNKNOWN
|
||||
self._use_variables = use_variables
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Load data init callbacks."""
|
||||
# Load data
|
||||
track_time_interval(hass, self._update_hub, SCAN_INTERVAL_HUB)
|
||||
self._update_hub(None)
|
||||
async_track_time_interval(
|
||||
self.hass, self._update_hub, SCAN_INTERVAL_HUB)
|
||||
yield from self.hass.async_add_job(self._update_hub, None)
|
||||
|
||||
if self._use_variables:
|
||||
track_time_interval(
|
||||
hass, self._update_variables, SCAN_INTERVAL_VARIABLES)
|
||||
self._update_variables(None)
|
||||
async_track_time_interval(
|
||||
self.hass, self._update_variables, SCAN_INTERVAL_VARIABLES)
|
||||
yield from self.hass.async_add_job(self._update_variables, None)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
|
@ -624,7 +622,9 @@ class HMHub(Entity):
|
|||
"""Retrieve latest state."""
|
||||
state = self._homematic.getServiceMessages(self._name)
|
||||
self._state = STATE_UNKNOWN if state is None else len(state)
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
if now:
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def _update_variables(self, now):
|
||||
"""Retrive all variable data and update hmvariable states."""
|
||||
|
@ -640,7 +640,7 @@ class HMHub(Entity):
|
|||
state_change = True
|
||||
self._variables.update({key: value})
|
||||
|
||||
if state_change:
|
||||
if state_change and now:
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def hm_set_variable(self, name, value):
|
||||
|
@ -662,16 +662,15 @@ class HMHub(Entity):
|
|||
class HMDevice(Entity):
|
||||
"""The HomeMatic device base object."""
|
||||
|
||||
def __init__(self, hass, config):
|
||||
def __init__(self, config):
|
||||
"""Initialize a generic HomeMatic device."""
|
||||
self.hass = hass
|
||||
self._homematic = hass.data[DATA_HOMEMATIC]
|
||||
self._name = config.get(ATTR_NAME)
|
||||
self._address = config.get(ATTR_ADDRESS)
|
||||
self._proxy = config.get(ATTR_PROXY)
|
||||
self._channel = config.get(ATTR_CHANNEL)
|
||||
self._state = config.get(ATTR_PARAM)
|
||||
self._data = {}
|
||||
self._homematic = None
|
||||
self._hmdevice = None
|
||||
self._connected = False
|
||||
self._available = False
|
||||
|
@ -680,6 +679,11 @@ class HMDevice(Entity):
|
|||
if self._state:
|
||||
self._state = self._state.upper()
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Load data init callbacks."""
|
||||
yield from self.hass.async_add_job(self.link_homematic)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return false. HomeMatic states are pushed by the XML-RPC Server."""
|
||||
|
@ -728,16 +732,13 @@ class HMDevice(Entity):
|
|||
return True
|
||||
|
||||
# Initialize
|
||||
self._homematic = self.hass.data[DATA_HOMEMATIC]
|
||||
self._hmdevice = self._homematic.devices[self._proxy][self._address]
|
||||
self._connected = True
|
||||
|
||||
try:
|
||||
# Initialize datapoints of this object
|
||||
self._init_data()
|
||||
if self.hass.data[DATA_DELAY]:
|
||||
# We optionally delay / pause loading of data to avoid
|
||||
# overloading of CCU / Homegear
|
||||
time.sleep(self.hass.data[DATA_DELAY])
|
||||
self._load_data_from_hm()
|
||||
|
||||
# Link events from pyhomematic
|
||||
|
|
|
@ -15,7 +15,7 @@ from homeassistant.components.image_processing import (
|
|||
from homeassistant.components.image_processing.microsoft_face_identify import (
|
||||
ImageProcessingFaceEntity)
|
||||
|
||||
REQUIREMENTS = ['face_recognition==0.2.2']
|
||||
REQUIREMENTS = ['face_recognition==1.0.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ from homeassistant.components.image_processing.microsoft_face_identify import (
|
|||
ImageProcessingFaceEntity)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['face_recognition==0.2.2']
|
||||
REQUIREMENTS = ['face_recognition==1.0.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
|
191
homeassistant/components/input_text.py
Executable file
191
homeassistant/components/input_text.py
Executable file
|
@ -0,0 +1,191 @@
|
|||
"""
|
||||
Component to offer a way to enter a value into a text box.
|
||||
|
||||
For more details about this component, please refer to the documentation
|
||||
at https://home-assistant.io/components/input_text/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, CONF_ICON, CONF_NAME)
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.restore_state import async_get_last_state
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = 'input_text'
|
||||
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||
|
||||
CONF_INITIAL = 'initial'
|
||||
CONF_MIN = 'min'
|
||||
CONF_MAX = 'max'
|
||||
|
||||
ATTR_VALUE = 'value'
|
||||
ATTR_MIN = 'min'
|
||||
ATTR_MAX = 'max'
|
||||
ATTR_PATTERN = 'pattern'
|
||||
|
||||
SERVICE_SET_VALUE = 'set_value'
|
||||
|
||||
SERVICE_SET_VALUE_SCHEMA = vol.Schema({
|
||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
|
||||
vol.Required(ATTR_VALUE): cv.string,
|
||||
})
|
||||
|
||||
|
||||
def _cv_input_text(cfg):
|
||||
"""Configure validation helper for input box (voluptuous)."""
|
||||
minimum = cfg.get(CONF_MIN)
|
||||
maximum = cfg.get(CONF_MAX)
|
||||
if minimum > maximum:
|
||||
raise vol.Invalid('Max len ({}) is not greater than min len ({})'
|
||||
.format(minimum, maximum))
|
||||
state = cfg.get(CONF_INITIAL)
|
||||
if state is not None and (len(state) < minimum or len(state) > maximum):
|
||||
raise vol.Invalid('Initial value {} length not in range {}-{}'
|
||||
.format(state, minimum, maximum))
|
||||
return cfg
|
||||
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
cv.slug: vol.All({
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_MIN, default=0): vol.Coerce(int),
|
||||
vol.Optional(CONF_MAX, default=100): vol.Coerce(int),
|
||||
vol.Optional(CONF_INITIAL, ''): cv.string,
|
||||
vol.Optional(CONF_ICON): cv.icon,
|
||||
vol.Optional(ATTR_UNIT_OF_MEASUREMENT): cv.string,
|
||||
vol.Optional(ATTR_PATTERN): cv.string,
|
||||
}, _cv_input_text)
|
||||
})
|
||||
}, required=True, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def set_value(hass, entity_id, value):
|
||||
"""Set input_text to value."""
|
||||
hass.services.call(DOMAIN, SERVICE_SET_VALUE, {
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
ATTR_VALUE: value,
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Set up an input text box."""
|
||||
component = EntityComponent(_LOGGER, DOMAIN, hass)
|
||||
|
||||
entities = []
|
||||
|
||||
for object_id, cfg in config[DOMAIN].items():
|
||||
name = cfg.get(CONF_NAME)
|
||||
minimum = cfg.get(CONF_MIN)
|
||||
maximum = cfg.get(CONF_MAX)
|
||||
initial = cfg.get(CONF_INITIAL)
|
||||
icon = cfg.get(CONF_ICON)
|
||||
unit = cfg.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||
pattern = cfg.get(ATTR_PATTERN)
|
||||
|
||||
entities.append(InputText(
|
||||
object_id, name, initial, minimum, maximum, icon, unit,
|
||||
pattern))
|
||||
|
||||
if not entities:
|
||||
return False
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_value_service(call):
|
||||
"""Handle a calls to the input box services."""
|
||||
target_inputs = component.async_extract_from_service(call)
|
||||
|
||||
tasks = [input_text.async_set_value(call.data[ATTR_VALUE])
|
||||
for input_text in target_inputs]
|
||||
if tasks:
|
||||
yield from asyncio.wait(tasks, loop=hass.loop)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SET_VALUE, async_set_value_service,
|
||||
schema=SERVICE_SET_VALUE_SCHEMA)
|
||||
|
||||
yield from component.async_add_entities(entities)
|
||||
return True
|
||||
|
||||
|
||||
class InputText(Entity):
|
||||
"""Represent a text box."""
|
||||
|
||||
def __init__(self, object_id, name, initial, minimum, maximum, icon,
|
||||
unit, pattern):
|
||||
"""Initialize a text input."""
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(object_id)
|
||||
self._name = name
|
||||
self._current_value = initial
|
||||
self._minimum = minimum
|
||||
self._maximum = maximum
|
||||
self._icon = icon
|
||||
self._unit = unit
|
||||
self._pattern = pattern
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""If entity should be polled."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the text input entity."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon to be used for this entity."""
|
||||
return self._icon
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the component."""
|
||||
return self._current_value
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit the value is expressed in."""
|
||||
return self._unit
|
||||
|
||||
@property
|
||||
def state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
return {
|
||||
ATTR_MIN: self._minimum,
|
||||
ATTR_MAX: self._maximum,
|
||||
ATTR_PATTERN: self._pattern,
|
||||
}
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""Run when entity about to be added to hass."""
|
||||
if self._current_value is not None:
|
||||
return
|
||||
|
||||
state = yield from async_get_last_state(self.hass, self.entity_id)
|
||||
value = state and state.state
|
||||
|
||||
# Check against None because value can be 0
|
||||
if value is not None and self._minimum <= len(value) <= self._maximum:
|
||||
self._current_value = value
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_set_value(self, value):
|
||||
"""Select new value."""
|
||||
if len(value) < self._minimum or len(value) > self._maximum:
|
||||
_LOGGER.warning("Invalid value: %s (length range %s - %s)",
|
||||
value, self._minimum, self._maximum)
|
||||
return
|
||||
self._current_value = value
|
||||
yield from self.async_update_ha_state()
|
|
@ -102,7 +102,7 @@ def common_attributes(entity):
|
|||
'address': 'INSTEON Address',
|
||||
'description': 'Description',
|
||||
'model': 'Model',
|
||||
'cat': 'Cagegory',
|
||||
'cat': 'Category',
|
||||
'subcat': 'Subcategory',
|
||||
'firmware': 'Firmware',
|
||||
'product_key': 'Product Key'
|
||||
|
|
|
@ -17,7 +17,7 @@ from homeassistant.helpers import discovery, config_validation as cv
|
|||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.typing import ConfigType, Dict # noqa
|
||||
|
||||
REQUIREMENTS = ['PyISY==1.0.7']
|
||||
REQUIREMENTS = ['PyISY==1.0.8']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
@ -1,495 +1,255 @@
|
|||
"""
|
||||
Support for KNX components.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
Connects to KNX platform.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/knx/
|
||||
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
import asyncio
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.helpers import discovery
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_STOP, CONF_HOST, CONF_PORT)
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, \
|
||||
CONF_HOST, CONF_PORT
|
||||
from homeassistant.helpers.script import Script
|
||||
|
||||
REQUIREMENTS = ['knxip==0.5']
|
||||
DOMAIN = "knx"
|
||||
DATA_KNX = "data_knx"
|
||||
CONF_KNX_CONFIG = "config_file"
|
||||
|
||||
CONF_KNX_ROUTING = "routing"
|
||||
CONF_KNX_TUNNELING = "tunneling"
|
||||
CONF_KNX_LOCAL_IP = "local_ip"
|
||||
CONF_KNX_FIRE_EVENT = "fire_event"
|
||||
CONF_KNX_FIRE_EVENT_FILTER = "fire_event_filter"
|
||||
|
||||
SERVICE_KNX_SEND = "send"
|
||||
SERVICE_KNX_ATTR_ADDRESS = "address"
|
||||
SERVICE_KNX_ATTR_PAYLOAD = "payload"
|
||||
|
||||
ATTR_DISCOVER_DEVICES = 'devices'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_HOST = '0.0.0.0'
|
||||
DEFAULT_PORT = 3671
|
||||
DOMAIN = 'knx'
|
||||
REQUIREMENTS = ['xknx==0.7.13']
|
||||
|
||||
EVENT_KNX_FRAME_RECEIVED = 'knx_frame_received'
|
||||
EVENT_KNX_FRAME_SEND = 'knx_frame_send'
|
||||
TUNNELING_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Optional(CONF_PORT): cv.port,
|
||||
vol.Required(CONF_KNX_LOCAL_IP): cv.string,
|
||||
})
|
||||
|
||||
KNXTUNNEL = None
|
||||
KNX_ADDRESS = "address"
|
||||
KNX_DATA = "data"
|
||||
KNX_GROUP_WRITE = "group_write"
|
||||
CONF_LISTEN = "listen"
|
||||
ROUTING_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_KNX_LOCAL_IP): cv.string,
|
||||
})
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_LISTEN, default=[]):
|
||||
vol.All(cv.ensure_list, [cv.string]),
|
||||
}),
|
||||
vol.Optional(CONF_KNX_CONFIG): cv.string,
|
||||
vol.Exclusive(CONF_KNX_ROUTING, 'connection_type'): ROUTING_SCHEMA,
|
||||
vol.Exclusive(CONF_KNX_TUNNELING, 'connection_type'):
|
||||
TUNNELING_SCHEMA,
|
||||
vol.Inclusive(CONF_KNX_FIRE_EVENT, 'fire_ev'):
|
||||
cv.boolean,
|
||||
vol.Inclusive(CONF_KNX_FIRE_EVENT_FILTER, 'fire_ev'):
|
||||
vol.All(
|
||||
cv.ensure_list,
|
||||
[cv.string])
|
||||
})
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
KNX_WRITE_SCHEMA = vol.Schema({
|
||||
vol.Required(KNX_ADDRESS): vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Required(KNX_DATA): vol.All(cv.ensure_list, [cv.byte])
|
||||
SERVICE_KNX_SEND_SCHEMA = vol.Schema({
|
||||
vol.Required(SERVICE_KNX_ATTR_ADDRESS): cv.string,
|
||||
vol.Required(SERVICE_KNX_ATTR_PAYLOAD): vol.Any(
|
||||
cv.positive_int, [cv.positive_int]),
|
||||
})
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Set up the connection to the KNX IP interface."""
|
||||
global KNXTUNNEL
|
||||
|
||||
from knxip.ip import KNXIPTunnel
|
||||
from knxip.core import KNXException, parse_group_address
|
||||
|
||||
host = config[DOMAIN].get(CONF_HOST)
|
||||
port = config[DOMAIN].get(CONF_PORT)
|
||||
|
||||
if host == '0.0.0.0':
|
||||
_LOGGER.debug("Will try to auto-detect KNX/IP gateway")
|
||||
|
||||
KNXTUNNEL = KNXIPTunnel(host, port)
|
||||
@asyncio.coroutine
|
||||
def async_setup(hass, config):
|
||||
"""Set up knx component."""
|
||||
from xknx.exceptions import XKNXException
|
||||
try:
|
||||
res = KNXTUNNEL.connect()
|
||||
_LOGGER.debug("Res = %s", res)
|
||||
if not res:
|
||||
_LOGGER.error("Could not connect to KNX/IP interface %s", host)
|
||||
return False
|
||||
hass.data[DATA_KNX] = KNXModule(hass, config)
|
||||
yield from hass.data[DATA_KNX].start()
|
||||
|
||||
except KNXException as ex:
|
||||
_LOGGER.exception("Can't connect to KNX/IP interface: %s", ex)
|
||||
KNXTUNNEL = None
|
||||
except XKNXException as ex:
|
||||
_LOGGER.exception("Can't connect to KNX interface: %s", ex)
|
||||
return False
|
||||
|
||||
_LOGGER.info("KNX IP tunnel to %s:%i established", host, port)
|
||||
for component, discovery_type in (
|
||||
('switch', 'Switch'),
|
||||
('climate', 'Climate'),
|
||||
('cover', 'Cover'),
|
||||
('light', 'Light'),
|
||||
('sensor', 'Sensor'),
|
||||
('binary_sensor', 'BinarySensor'),
|
||||
('notify', 'Notification')):
|
||||
found_devices = _get_devices(hass, discovery_type)
|
||||
hass.async_add_job(
|
||||
discovery.async_load_platform(hass, component, DOMAIN, {
|
||||
ATTR_DISCOVER_DEVICES: found_devices
|
||||
}, config))
|
||||
|
||||
descriptions = load_yaml_config_file(
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
def received_knx_event(address, data):
|
||||
"""Process received KNX message."""
|
||||
if len(data) == 1:
|
||||
data = data[0]
|
||||
hass.bus.fire('knx_event', {
|
||||
'address': address,
|
||||
'data': data
|
||||
})
|
||||
|
||||
for listen in config[DOMAIN].get(CONF_LISTEN):
|
||||
_LOGGER.debug("Registering listener for %s", listen)
|
||||
try:
|
||||
KNXTUNNEL.register_listener(parse_group_address(listen),
|
||||
received_knx_event)
|
||||
except KNXException as knxexception:
|
||||
_LOGGER.error("Can't register KNX listener for address %s (%s)",
|
||||
listen, knxexception)
|
||||
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, close_tunnel)
|
||||
|
||||
# Listen to KNX events and send them to the bus
|
||||
def handle_group_write(call):
|
||||
"""Bridge knx_frame_send events to the KNX bus."""
|
||||
# parameters are pre-validated using KNX_WRITE_SCHEMA
|
||||
addrlist = call.data.get("address")
|
||||
knxdata = call.data.get("data")
|
||||
|
||||
knxaddrlist = []
|
||||
for addr in addrlist:
|
||||
try:
|
||||
_LOGGER.debug("Found %s", addr)
|
||||
knxaddr = int(addr)
|
||||
except ValueError:
|
||||
knxaddr = None
|
||||
|
||||
if knxaddr is None:
|
||||
try:
|
||||
knxaddr = parse_group_address(addr)
|
||||
except KNXException:
|
||||
_LOGGER.error("KNX address format incorrect: %s", addr)
|
||||
|
||||
knxaddrlist.append(knxaddr)
|
||||
|
||||
for addr in knxaddrlist:
|
||||
KNXTUNNEL.group_write(addr, knxdata)
|
||||
|
||||
# Listen for when knx_frame_send event is fired
|
||||
hass.services.register(DOMAIN,
|
||||
KNX_GROUP_WRITE,
|
||||
handle_group_write,
|
||||
descriptions[DOMAIN][KNX_GROUP_WRITE],
|
||||
schema=KNX_WRITE_SCHEMA)
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_KNX_SEND,
|
||||
hass.data[DATA_KNX].service_send_to_knx_bus,
|
||||
schema=SERVICE_KNX_SEND_SCHEMA)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def close_tunnel(_data):
|
||||
"""Close the NKX tunnel connection on shutdown."""
|
||||
global KNXTUNNEL
|
||||
|
||||
KNXTUNNEL.disconnect()
|
||||
KNXTUNNEL = None
|
||||
def _get_devices(hass, discovery_type):
|
||||
return list(
|
||||
map(lambda device: device.name,
|
||||
filter(
|
||||
lambda device: type(device).__name__ == discovery_type,
|
||||
hass.data[DATA_KNX].xknx.devices)))
|
||||
|
||||
|
||||
class KNXConfig(object):
|
||||
"""Handle the fetching of configuration from the config file."""
|
||||
|
||||
def __init__(self, config):
|
||||
"""Initialize the configuration."""
|
||||
from knxip.core import parse_group_address
|
||||
|
||||
self.config = config
|
||||
self.should_poll = config.get('poll', True)
|
||||
if config.get('address'):
|
||||
self._address = parse_group_address(config.get('address'))
|
||||
else:
|
||||
self._address = None
|
||||
if self.config.get('state_address'):
|
||||
self._state_address = parse_group_address(
|
||||
self.config.get('state_address'))
|
||||
else:
|
||||
self._state_address = None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name given to the entity."""
|
||||
return self.config['name']
|
||||
|
||||
@property
|
||||
def address(self):
|
||||
"""Return the address of the device as an integer value.
|
||||
|
||||
3 types of addresses are supported:
|
||||
integer - 0-65535
|
||||
2 level - a/b
|
||||
3 level - a/b/c
|
||||
"""
|
||||
return self._address
|
||||
|
||||
@property
|
||||
def state_address(self):
|
||||
"""Return the group address the device sends its current state to.
|
||||
|
||||
Some KNX devices can send the current state to a seperate
|
||||
group address. This makes send e.g. when an actuator can
|
||||
be switched but also have a timer functionality.
|
||||
"""
|
||||
return self._state_address
|
||||
|
||||
|
||||
class KNXGroupAddress(Entity):
|
||||
"""Representation of devices connected to a KNX group address."""
|
||||
class KNXModule(object):
|
||||
"""Representation of KNX Object."""
|
||||
|
||||
def __init__(self, hass, config):
|
||||
"""Initialize the device."""
|
||||
self._config = config
|
||||
self._state = False
|
||||
self._data = None
|
||||
_LOGGER.debug(
|
||||
"Initalizing KNX group address for %s (%s)",
|
||||
self.name, self.address
|
||||
)
|
||||
"""Initialization of KNXModule."""
|
||||
self.hass = hass
|
||||
self.config = config
|
||||
self.initialized = False
|
||||
self.init_xknx()
|
||||
self.register_callbacks()
|
||||
|
||||
def handle_knx_message(addr, data):
|
||||
"""Handle an incoming KNX frame.
|
||||
def init_xknx(self):
|
||||
"""Initialization of KNX object."""
|
||||
from xknx import XKNX
|
||||
self.xknx = XKNX(
|
||||
config=self.config_file(),
|
||||
loop=self.hass.loop)
|
||||
|
||||
Handle an incoming frame and update our status if it contains
|
||||
information relating to this device.
|
||||
"""
|
||||
if (addr == self.state_address) or (addr == self.address):
|
||||
self._state = data[0]
|
||||
self.schedule_update_ha_state()
|
||||
@asyncio.coroutine
|
||||
def start(self):
|
||||
"""Start KNX object. Connect to tunneling or Routing device."""
|
||||
connection_config = self.connection_config()
|
||||
yield from self.xknx.start(
|
||||
state_updater=True,
|
||||
connection_config=connection_config)
|
||||
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.stop)
|
||||
self.initialized = True
|
||||
|
||||
KNXTUNNEL.register_listener(self.address, handle_knx_message)
|
||||
if self.state_address:
|
||||
KNXTUNNEL.register_listener(self.state_address, handle_knx_message)
|
||||
@asyncio.coroutine
|
||||
def stop(self, event):
|
||||
"""Stop KNX object. Disconnect from tunneling or Routing device."""
|
||||
yield from self.xknx.stop()
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the entity's display name."""
|
||||
return self._config.name
|
||||
def config_file(self):
|
||||
"""Resolve and return the full path of xknx.yaml if configured."""
|
||||
config_file = self.config[DOMAIN].get(CONF_KNX_CONFIG)
|
||||
if not config_file:
|
||||
return None
|
||||
if not config_file.startswith("/"):
|
||||
return self.hass.config.path(config_file)
|
||||
return config_file
|
||||
|
||||
@property
|
||||
def config(self):
|
||||
"""Return the entity's configuration."""
|
||||
return self._config
|
||||
def connection_config(self):
|
||||
"""Return the connection_config."""
|
||||
if CONF_KNX_TUNNELING in self.config[DOMAIN]:
|
||||
return self.connection_config_tunneling()
|
||||
elif CONF_KNX_ROUTING in self.config[DOMAIN]:
|
||||
return self.connection_config_routing()
|
||||
return self.connection_config_auto()
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return the state of the polling, if needed."""
|
||||
return self._config.should_poll
|
||||
def connection_config_routing(self):
|
||||
"""Return the connection_config if routing is configured."""
|
||||
from xknx.io import ConnectionConfig, ConnectionType
|
||||
local_ip = \
|
||||
self.config[DOMAIN][CONF_KNX_ROUTING].get(CONF_KNX_LOCAL_IP)
|
||||
return ConnectionConfig(
|
||||
connection_type=ConnectionType.ROUTING,
|
||||
local_ip=local_ip)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return True if the value is not 0 is on, else False."""
|
||||
return self._state != 0
|
||||
def connection_config_tunneling(self):
|
||||
"""Return the connection_config if tunneling is configured."""
|
||||
from xknx.io import ConnectionConfig, ConnectionType, \
|
||||
DEFAULT_MCAST_PORT
|
||||
gateway_ip = \
|
||||
self.config[DOMAIN][CONF_KNX_TUNNELING].get(CONF_HOST)
|
||||
gateway_port = \
|
||||
self.config[DOMAIN][CONF_KNX_TUNNELING].get(CONF_PORT)
|
||||
local_ip = \
|
||||
self.config[DOMAIN][CONF_KNX_TUNNELING].get(CONF_KNX_LOCAL_IP)
|
||||
if gateway_port is None:
|
||||
gateway_port = DEFAULT_MCAST_PORT
|
||||
return ConnectionConfig(
|
||||
connection_type=ConnectionType.TUNNELING,
|
||||
gateway_ip=gateway_ip,
|
||||
gateway_port=gateway_port,
|
||||
local_ip=local_ip)
|
||||
|
||||
@property
|
||||
def address(self):
|
||||
"""Return the KNX group address."""
|
||||
return self._config.address
|
||||
def connection_config_auto(self):
|
||||
"""Return the connection_config if auto is configured."""
|
||||
# pylint: disable=no-self-use
|
||||
from xknx.io import ConnectionConfig
|
||||
return ConnectionConfig()
|
||||
|
||||
@property
|
||||
def state_address(self):
|
||||
"""Return the KNX group address."""
|
||||
return self._config.state_address
|
||||
def register_callbacks(self):
|
||||
"""Register callbacks within XKNX object."""
|
||||
if CONF_KNX_FIRE_EVENT in self.config[DOMAIN] and \
|
||||
self.config[DOMAIN][CONF_KNX_FIRE_EVENT]:
|
||||
from xknx.knx import AddressFilter
|
||||
address_filters = list(map(
|
||||
AddressFilter,
|
||||
self.config[DOMAIN][CONF_KNX_FIRE_EVENT_FILTER]))
|
||||
self.xknx.telegram_queue.register_telegram_received_cb(
|
||||
self.telegram_received_cb, address_filters)
|
||||
|
||||
@property
|
||||
def cache(self):
|
||||
"""Return the name given to the entity."""
|
||||
return self._config.config.get('cache', True)
|
||||
|
||||
def group_write(self, value):
|
||||
"""Write to the group address."""
|
||||
KNXTUNNEL.group_write(self.address, [value])
|
||||
|
||||
def update(self):
|
||||
"""Get the state from KNX bus or cache."""
|
||||
from knxip.core import KNXException
|
||||
|
||||
try:
|
||||
if self.state_address:
|
||||
res = KNXTUNNEL.group_read(
|
||||
self.state_address, use_cache=self.cache)
|
||||
else:
|
||||
res = KNXTUNNEL.group_read(self.address, use_cache=self.cache)
|
||||
|
||||
if res:
|
||||
self._state = res[0]
|
||||
self._data = res
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"%s: unable to read from KNX address: %s (None)",
|
||||
self.name, self.address
|
||||
)
|
||||
|
||||
except KNXException:
|
||||
_LOGGER.exception(
|
||||
"%s: unable to read from KNX address: %s",
|
||||
self.name, self.address
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
class KNXMultiAddressDevice(Entity):
|
||||
"""Representation of devices connected to a multiple KNX group address.
|
||||
|
||||
This is needed for devices like dimmers or shutter actuators as they have
|
||||
to be controlled by multiple group addresses.
|
||||
"""
|
||||
|
||||
def __init__(self, hass, config, required, optional=None):
|
||||
"""Initialize the device.
|
||||
|
||||
The namelist argument lists the required addresses. E.g. for a dimming
|
||||
actuators, the namelist might look like:
|
||||
onoff_address: 0/0/1
|
||||
brightness_address: 0/0/2
|
||||
"""
|
||||
from knxip.core import parse_group_address, KNXException
|
||||
|
||||
self.names = {}
|
||||
self.values = {}
|
||||
|
||||
self._config = config
|
||||
self._state = False
|
||||
self._data = None
|
||||
_LOGGER.debug(
|
||||
"%s: initalizing KNX multi address device",
|
||||
self.name
|
||||
)
|
||||
|
||||
settings = self._config.config
|
||||
if config.address:
|
||||
_LOGGER.debug(
|
||||
"%s: base address: address=%s",
|
||||
self.name, settings.get('address')
|
||||
)
|
||||
self.names[config.address] = 'base'
|
||||
if config.state_address:
|
||||
_LOGGER.debug(
|
||||
"%s, state address: state_address=%s",
|
||||
self.name, settings.get('state_address')
|
||||
)
|
||||
self.names[config.state_address] = 'state'
|
||||
|
||||
# parse required addresses
|
||||
for name in required:
|
||||
paramname = '{}{}'.format(name, '_address')
|
||||
addr = settings.get(paramname)
|
||||
if addr is None:
|
||||
_LOGGER.error(
|
||||
"%s: Required KNX group address %s missing",
|
||||
self.name, paramname
|
||||
)
|
||||
raise KNXException(
|
||||
"%s: Group address for {} missing in "
|
||||
"configuration for {}".format(
|
||||
self.name, paramname
|
||||
)
|
||||
)
|
||||
_LOGGER.debug(
|
||||
"%s: (required parameter) %s=%s",
|
||||
self.name, paramname, addr
|
||||
)
|
||||
addr = parse_group_address(addr)
|
||||
self.names[addr] = name
|
||||
|
||||
# parse optional addresses
|
||||
for name in optional:
|
||||
paramname = '{}{}'.format(name, '_address')
|
||||
addr = settings.get(paramname)
|
||||
_LOGGER.debug(
|
||||
"%s: (optional parameter) %s=%s",
|
||||
self.name, paramname, addr
|
||||
)
|
||||
if addr:
|
||||
try:
|
||||
addr = parse_group_address(addr)
|
||||
except KNXException:
|
||||
_LOGGER.exception(
|
||||
"%s: cannot parse group address %s",
|
||||
self.name, addr
|
||||
)
|
||||
self.names[addr] = name
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the entity's display name."""
|
||||
return self._config.name
|
||||
|
||||
@property
|
||||
def config(self):
|
||||
"""Return the entity's configuration."""
|
||||
return self._config
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return the state of the polling, if needed."""
|
||||
return self._config.should_poll
|
||||
|
||||
@property
|
||||
def cache(self):
|
||||
"""Return the name given to the entity."""
|
||||
return self._config.config.get('cache', True)
|
||||
|
||||
def has_attribute(self, name):
|
||||
"""Check if the attribute with the given name is defined.
|
||||
|
||||
This is mostly important for optional addresses.
|
||||
"""
|
||||
for attributename in self.names.values():
|
||||
if attributename == name:
|
||||
return True
|
||||
@asyncio.coroutine
|
||||
def telegram_received_cb(self, telegram):
|
||||
"""Callback invoked after a KNX telegram was received."""
|
||||
self.hass.bus.fire('knx_event', {
|
||||
'address': telegram.group_address.str(),
|
||||
'data': telegram.payload.value
|
||||
})
|
||||
# False signals XKNX to proceed with processing telegrams.
|
||||
return False
|
||||
|
||||
def set_percentage(self, name, percentage):
|
||||
"""Set a percentage in knx for a given attribute.
|
||||
@asyncio.coroutine
|
||||
def service_send_to_knx_bus(self, call):
|
||||
"""Service for sending an arbitray KNX message to the KNX bus."""
|
||||
from xknx.knx import Telegram, Address, DPTBinary, DPTArray
|
||||
attr_payload = call.data.get(SERVICE_KNX_ATTR_PAYLOAD)
|
||||
attr_address = call.data.get(SERVICE_KNX_ATTR_ADDRESS)
|
||||
|
||||
DPT_Scaling / DPT 5.001 is a single byte scaled percentage
|
||||
"""
|
||||
percentage = abs(percentage) # only accept positive values
|
||||
scaled_value = percentage * 255 / 100
|
||||
value = min(255, scaled_value)
|
||||
return self.set_int_value(name, value)
|
||||
def calculate_payload(attr_payload):
|
||||
"""Calculate payload depending on type of attribute."""
|
||||
if isinstance(attr_payload, int):
|
||||
return DPTBinary(attr_payload)
|
||||
return DPTArray(attr_payload)
|
||||
payload = calculate_payload(attr_payload)
|
||||
address = Address(attr_address)
|
||||
|
||||
def get_percentage(self, name):
|
||||
"""Get a percentage from knx for a given attribute.
|
||||
telegram = Telegram()
|
||||
telegram.payload = payload
|
||||
telegram.group_address = address
|
||||
yield from self.xknx.telegrams.put(telegram)
|
||||
|
||||
DPT_Scaling / DPT 5.001 is a single byte scaled percentage
|
||||
"""
|
||||
value = self.get_int_value(name)
|
||||
percentage = round(value * 100 / 255)
|
||||
return percentage
|
||||
|
||||
def set_int_value(self, name, value, num_bytes=1):
|
||||
"""Set an integer value for a given attribute."""
|
||||
# KNX packets are big endian
|
||||
value = round(value) # only accept integers
|
||||
b_value = value.to_bytes(num_bytes, byteorder='big')
|
||||
return self.set_value(name, list(b_value))
|
||||
class KNXAutomation():
|
||||
"""Wrapper around xknx.devices.ActionCallback object.."""
|
||||
|
||||
def get_int_value(self, name):
|
||||
"""Get an integer value for a given attribute."""
|
||||
# KNX packets are big endian
|
||||
summed_value = 0
|
||||
raw_value = self.value(name)
|
||||
try:
|
||||
# convert raw value in bytes
|
||||
for val in raw_value:
|
||||
summed_value *= 256
|
||||
summed_value += val
|
||||
except TypeError:
|
||||
# pknx returns a non-iterable type for unsuccessful reads
|
||||
pass
|
||||
def __init__(self, hass, device, hook, action, counter=1):
|
||||
"""Initialize Automation class."""
|
||||
self.hass = hass
|
||||
self.device = device
|
||||
script_name = "{} turn ON script".format(device.get_name())
|
||||
self.script = Script(hass, action, script_name)
|
||||
|
||||
return summed_value
|
||||
|
||||
def value(self, name):
|
||||
"""Return the value to a given named attribute."""
|
||||
from knxip.core import KNXException
|
||||
|
||||
addr = None
|
||||
for attributeaddress, attributename in self.names.items():
|
||||
if attributename == name:
|
||||
addr = attributeaddress
|
||||
|
||||
if addr is None:
|
||||
_LOGGER.error("%s: attribute '%s' undefined",
|
||||
self.name, name)
|
||||
_LOGGER.debug(
|
||||
"%s: defined attributes: %s",
|
||||
self.name, str(self.names)
|
||||
)
|
||||
return False
|
||||
|
||||
try:
|
||||
res = KNXTUNNEL.group_read(addr, use_cache=self.cache)
|
||||
except KNXException:
|
||||
_LOGGER.exception(
|
||||
"%s: unable to read from KNX address: %s",
|
||||
self.name, addr
|
||||
)
|
||||
return False
|
||||
|
||||
return res
|
||||
|
||||
def set_value(self, name, value):
|
||||
"""Set the value of a given named attribute."""
|
||||
from knxip.core import KNXException
|
||||
|
||||
addr = None
|
||||
for attributeaddress, attributename in self.names.items():
|
||||
if attributename == name:
|
||||
addr = attributeaddress
|
||||
|
||||
if addr is None:
|
||||
_LOGGER.error("%s: attribute '%s' undefined",
|
||||
self.name, name)
|
||||
_LOGGER.debug(
|
||||
"%s: defined attributes: %s",
|
||||
self.name, str(self.names)
|
||||
)
|
||||
return False
|
||||
|
||||
try:
|
||||
KNXTUNNEL.group_write(addr, value)
|
||||
except KNXException:
|
||||
_LOGGER.exception(
|
||||
"%s: unable to write to KNX address: %s",
|
||||
self.name, addr
|
||||
)
|
||||
return False
|
||||
|
||||
return True
|
||||
import xknx
|
||||
self.action = xknx.devices.ActionCallback(
|
||||
hass.data[DATA_KNX].xknx,
|
||||
self.script.async_run,
|
||||
hook=hook,
|
||||
counter=counter)
|
||||
device.actions.append(self.action)
|
||||
|
|
|
@ -24,8 +24,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
|
||||
devices = []
|
||||
for conf in discovery_info[ATTR_DISCOVER_DEVICES]:
|
||||
new_device = HMLight(hass, conf)
|
||||
new_device.link_homematic()
|
||||
new_device = HMLight(conf)
|
||||
devices.append(new_device)
|
||||
|
||||
add_devices(devices)
|
||||
|
|
|
@ -83,6 +83,7 @@ SCENE_SCHEMA = vol.Schema({
|
|||
})
|
||||
|
||||
ATTR_IS_HUE_GROUP = "is_hue_group"
|
||||
GROUP_NAME_ALL_HUE_LIGHTS = "All Hue Lights"
|
||||
|
||||
|
||||
def _find_host_from_config(hass, filename=PHUE_CONFIG_FILE):
|
||||
|
@ -203,6 +204,21 @@ def setup_bridge(host, hass, add_devices, filename, allow_unreachable,
|
|||
_LOGGER.error("Got unexpected result from Hue API")
|
||||
return
|
||||
|
||||
if not skip_groups:
|
||||
# Group ID 0 is a special group in the hub for all lights, but it
|
||||
# is not returned by get_api() so explicity get it and include it.
|
||||
# See https://developers.meethue.com/documentation/
|
||||
# groups-api#21_get_all_groups
|
||||
_LOGGER.debug("Getting group 0 from bridge")
|
||||
all_lights = bridge.get_group(0)
|
||||
if not isinstance(all_lights, dict):
|
||||
_LOGGER.error("Got unexpected result from Hue API for group 0")
|
||||
return
|
||||
# Hue hub returns name of group 0 as "Group 0", so rename
|
||||
# for ease of use in HA.
|
||||
all_lights['name'] = GROUP_NAME_ALL_HUE_LIGHTS
|
||||
api_groups["0"] = all_lights
|
||||
|
||||
new_lights = []
|
||||
|
||||
api_name = api.get('config').get('name')
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
"""
|
||||
Support KNX Lighting actuators.
|
||||
Support for KNX/IP lights.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/Light.knx/
|
||||
https://home-assistant.io/components/light.knx/
|
||||
"""
|
||||
import logging
|
||||
import asyncio
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.knx import (KNXConfig, KNXMultiAddressDevice)
|
||||
from homeassistant.components.light import (Light, PLATFORM_SCHEMA,
|
||||
SUPPORT_BRIGHTNESS,
|
||||
ATTR_BRIGHTNESS)
|
||||
from homeassistant.components.knx import DATA_KNX, ATTR_DISCOVER_DEVICES
|
||||
from homeassistant.components.light import PLATFORM_SCHEMA, Light, \
|
||||
SUPPORT_BRIGHTNESS, ATTR_BRIGHTNESS
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
CONF_ADDRESS = 'address'
|
||||
|
@ -19,8 +19,6 @@ CONF_STATE_ADDRESS = 'state_address'
|
|||
CONF_BRIGHTNESS_ADDRESS = 'brightness_address'
|
||||
CONF_BRIGHTNESS_STATE_ADDRESS = 'brightness_state_address'
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = 'KNX Light'
|
||||
DEPENDENCIES = ['knx']
|
||||
|
||||
|
@ -33,84 +31,136 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the KNX light platform."""
|
||||
add_devices([KNXLight(hass, KNXConfig(config))])
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, add_devices,
|
||||
discovery_info=None):
|
||||
"""Set up light(s) for KNX platform."""
|
||||
if DATA_KNX not in hass.data \
|
||||
or not hass.data[DATA_KNX].initialized:
|
||||
return False
|
||||
|
||||
if discovery_info is not None:
|
||||
async_add_devices_discovery(hass, discovery_info, add_devices)
|
||||
else:
|
||||
async_add_devices_config(hass, config, add_devices)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class KNXLight(KNXMultiAddressDevice, Light):
|
||||
"""Representation of a KNX Light device."""
|
||||
@callback
|
||||
def async_add_devices_discovery(hass, discovery_info, add_devices):
|
||||
"""Set up lights for KNX platform configured via xknx.yaml."""
|
||||
entities = []
|
||||
for device_name in discovery_info[ATTR_DISCOVER_DEVICES]:
|
||||
device = hass.data[DATA_KNX].xknx.devices[device_name]
|
||||
entities.append(KNXLight(hass, device))
|
||||
add_devices(entities)
|
||||
|
||||
def __init__(self, hass, config):
|
||||
"""Initialize the cover."""
|
||||
KNXMultiAddressDevice.__init__(
|
||||
self, hass, config,
|
||||
[], # required
|
||||
optional=['state', 'brightness', 'brightness_state']
|
||||
)
|
||||
self._hass = hass
|
||||
self._supported_features = 0
|
||||
|
||||
if CONF_BRIGHTNESS_ADDRESS in config.config:
|
||||
_LOGGER.debug("%s is dimmable", self.name)
|
||||
self._supported_features = self._supported_features | \
|
||||
SUPPORT_BRIGHTNESS
|
||||
self._brightness = None
|
||||
@callback
|
||||
def async_add_devices_config(hass, config, add_devices):
|
||||
"""Set up light for KNX platform configured within plattform."""
|
||||
import xknx
|
||||
light = xknx.devices.Light(
|
||||
hass.data[DATA_KNX].xknx,
|
||||
name=config.get(CONF_NAME),
|
||||
group_address_switch=config.get(CONF_ADDRESS),
|
||||
group_address_switch_state=config.get(CONF_STATE_ADDRESS),
|
||||
group_address_brightness=config.get(CONF_BRIGHTNESS_ADDRESS),
|
||||
group_address_brightness_state=config.get(
|
||||
CONF_BRIGHTNESS_STATE_ADDRESS))
|
||||
hass.data[DATA_KNX].xknx.devices.add(light)
|
||||
add_devices([KNXLight(hass, light)])
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Turn the switch on.
|
||||
|
||||
This sends a value 1 to the group address of the device
|
||||
"""
|
||||
_LOGGER.debug("%s: turn on", self.name)
|
||||
self.set_value('base', [1])
|
||||
self._state = 1
|
||||
class KNXLight(Light):
|
||||
"""Representation of a KNX light."""
|
||||
|
||||
if ATTR_BRIGHTNESS in kwargs:
|
||||
self._brightness = kwargs[ATTR_BRIGHTNESS]
|
||||
_LOGGER.debug("turn_on requested brightness for light: %s is: %s ",
|
||||
self.name, self._brightness)
|
||||
assert self._brightness <= 255
|
||||
self.set_value("brightness", [self._brightness])
|
||||
def __init__(self, hass, device):
|
||||
"""Initialization of KNXLight."""
|
||||
self.device = device
|
||||
self.hass = hass
|
||||
self.async_register_callbacks()
|
||||
|
||||
if not self.should_poll:
|
||||
self.schedule_update_ha_state()
|
||||
@callback
|
||||
def async_register_callbacks(self):
|
||||
"""Register callbacks to update hass after device was changed."""
|
||||
@asyncio.coroutine
|
||||
def after_update_callback(device):
|
||||
"""Callback after device was updated."""
|
||||
# pylint: disable=unused-argument
|
||||
yield from self.async_update_ha_state()
|
||||
self.device.register_device_updated_cb(after_update_callback)
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
"""Turn the switch off.
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the KNX device."""
|
||||
return self.device.name
|
||||
|
||||
This sends a value 1 to the group address of the device
|
||||
"""
|
||||
_LOGGER.debug("%s: turn off", self.name)
|
||||
self.set_value('base', [0])
|
||||
self._state = 0
|
||||
if not self.should_poll:
|
||||
self.schedule_update_ha_state()
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""No polling needed within KNX."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
"""Return the brightness of this light between 0..255."""
|
||||
return self.device.brightness \
|
||||
if self.device.supports_dimming else \
|
||||
None
|
||||
|
||||
@property
|
||||
def xy_color(self):
|
||||
"""Return the XY color value [float, float]."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def rgb_color(self):
|
||||
"""Return the RBG color value."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def color_temp(self):
|
||||
"""Return the CT color temperature."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def white_value(self):
|
||||
"""Return the white value of this light between 0..255."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def effect_list(self):
|
||||
"""Return the list of supported effects."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def effect(self):
|
||||
"""Return the current effect."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return True if the value is not 0 is on, else False."""
|
||||
return self._state != 0
|
||||
"""Return true if light is on."""
|
||||
return self.device.state
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
return self._supported_features
|
||||
flags = 0
|
||||
if self.device.supports_dimming:
|
||||
flags |= SUPPORT_BRIGHTNESS
|
||||
return flags
|
||||
|
||||
def update(self):
|
||||
"""Update device state."""
|
||||
super().update()
|
||||
if self.has_attribute('brightness_state'):
|
||||
value = self.value('brightness_state')
|
||||
if value is not None:
|
||||
self._brightness = int.from_bytes(value, byteorder='little')
|
||||
_LOGGER.debug("%s: brightness = %d",
|
||||
self.name, self._brightness)
|
||||
@asyncio.coroutine
|
||||
def async_turn_on(self, **kwargs):
|
||||
"""Turn the light on."""
|
||||
if ATTR_BRIGHTNESS in kwargs and self.device.supports_dimming:
|
||||
yield from self.device.set_brightness(int(kwargs[ATTR_BRIGHTNESS]))
|
||||
else:
|
||||
yield from self.device.set_on()
|
||||
|
||||
if self.has_attribute('state'):
|
||||
self._state = self.value("state")[0]
|
||||
_LOGGER.debug("%s: state = %d", self.name, self._state)
|
||||
|
||||
def should_poll(self):
|
||||
"""No polling needed for a KNX light."""
|
||||
return False
|
||||
@asyncio.coroutine
|
||||
def async_turn_off(self, **kwargs):
|
||||
"""Turn the light off."""
|
||||
yield from self.device.set_off()
|
||||
|
|
|
@ -7,7 +7,7 @@ https://home-assistant.io/components/light.lutron_caseta/
|
|||
import logging
|
||||
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light)
|
||||
ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light, DOMAIN)
|
||||
from homeassistant.components.light.lutron import (
|
||||
to_hass_level, to_lutron_level)
|
||||
from homeassistant.components.lutron_caseta import (
|
||||
|
@ -23,7 +23,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
"""Set up the Lutron Caseta lights."""
|
||||
devs = []
|
||||
bridge = hass.data[LUTRON_CASETA_SMARTBRIDGE]
|
||||
light_devices = bridge.get_devices_by_types(["WallDimmer", "PlugInDimmer"])
|
||||
light_devices = bridge.get_devices_by_domain(DOMAIN)
|
||||
for light_device in light_devices:
|
||||
dev = LutronCasetaLight(light_device, bridge)
|
||||
devs.append(dev)
|
||||
|
|
|
@ -39,6 +39,7 @@ CONF_EFFECT_COMMAND_TOPIC = 'effect_command_topic'
|
|||
CONF_EFFECT_LIST = 'effect_list'
|
||||
CONF_EFFECT_STATE_TOPIC = 'effect_state_topic'
|
||||
CONF_EFFECT_VALUE_TEMPLATE = 'effect_value_template'
|
||||
CONF_RGB_COMMAND_TEMPLATE = 'rgb_command_template'
|
||||
CONF_RGB_COMMAND_TOPIC = 'rgb_command_topic'
|
||||
CONF_RGB_STATE_TOPIC = 'rgb_state_topic'
|
||||
CONF_RGB_VALUE_TEMPLATE = 'rgb_value_template'
|
||||
|
@ -75,6 +76,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({
|
|||
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
|
||||
vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string,
|
||||
vol.Optional(CONF_RGB_COMMAND_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_RGB_COMMAND_TOPIC): mqtt.valid_publish_topic,
|
||||
vol.Optional(CONF_RGB_STATE_TOPIC): mqtt.valid_subscribe_topic,
|
||||
vol.Optional(CONF_RGB_VALUE_TEMPLATE): cv.template,
|
||||
|
@ -125,6 +127,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
|||
CONF_COLOR_TEMP: config.get(CONF_COLOR_TEMP_VALUE_TEMPLATE),
|
||||
CONF_EFFECT: config.get(CONF_EFFECT_VALUE_TEMPLATE),
|
||||
CONF_RGB: config.get(CONF_RGB_VALUE_TEMPLATE),
|
||||
CONF_RGB_COMMAND_TEMPLATE: config.get(CONF_RGB_COMMAND_TEMPLATE),
|
||||
CONF_STATE: config.get(CONF_STATE_VALUE_TEMPLATE),
|
||||
CONF_WHITE_VALUE: config.get(CONF_WHITE_VALUE_TEMPLATE),
|
||||
CONF_XY: config.get(CONF_XY_VALUE_TEMPLATE),
|
||||
|
@ -397,10 +400,17 @@ class MqttLight(Light):
|
|||
if ATTR_RGB_COLOR in kwargs and \
|
||||
self._topic[CONF_RGB_COMMAND_TOPIC] is not None:
|
||||
|
||||
tpl = self._templates[CONF_RGB_COMMAND_TEMPLATE]
|
||||
if tpl:
|
||||
colors = {'red', 'green', 'blue'}
|
||||
variables = {key: val for key, val in
|
||||
zip(colors, kwargs[ATTR_RGB_COLOR])}
|
||||
rgb_color_str = tpl.async_render(variables)
|
||||
else:
|
||||
rgb_color_str = '{},{},{}'.format(*kwargs[ATTR_RGB_COLOR])
|
||||
mqtt.async_publish(
|
||||
self.hass, self._topic[CONF_RGB_COMMAND_TOPIC],
|
||||
'{},{},{}'.format(*kwargs[ATTR_RGB_COLOR]), self._qos,
|
||||
self._retain)
|
||||
rgb_color_str, self._qos, self._retain)
|
||||
|
||||
if self._optimistic_rgb:
|
||||
self._rgb = kwargs[ATTR_RGB_COLOR]
|
||||
|
|
|
@ -23,7 +23,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
"""Set up the RFXtrx platform."""
|
||||
import RFXtrx as rfxtrxmod
|
||||
|
||||
lights = rfxtrx.get_devices_from_config(config, RfxtrxLight, hass)
|
||||
lights = rfxtrx.get_devices_from_config(config, RfxtrxLight)
|
||||
add_devices(lights)
|
||||
|
||||
def light_update(event):
|
||||
|
@ -32,7 +32,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
not event.device.known_to_be_dimmable:
|
||||
return
|
||||
|
||||
new_device = rfxtrx.get_new_device(event, config, RfxtrxLight, hass)
|
||||
new_device = rfxtrx.get_new_device(event, config, RfxtrxLight)
|
||||
if new_device:
|
||||
add_devices([new_device])
|
||||
|
||||
|
|
|
@ -9,9 +9,10 @@ import logging
|
|||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, SUPPORT_BRIGHTNESS,
|
||||
SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR, Light)
|
||||
from homeassistant.components.light import \
|
||||
PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA
|
||||
from homeassistant.components.tradfri import KEY_GATEWAY, KEY_TRADFRI_GROUPS
|
||||
from homeassistant.components.light import (
|
||||
PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA)
|
||||
from homeassistant.components.tradfri import (
|
||||
KEY_GATEWAY, KEY_TRADFRI_GROUPS, KEY_API)
|
||||
from homeassistant.util import color as color_util
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
@ -19,9 +20,7 @@ _LOGGER = logging.getLogger(__name__)
|
|||
DEPENDENCIES = ['tradfri']
|
||||
PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA
|
||||
IKEA = 'IKEA of Sweden'
|
||||
ALLOWED_TEMPERATURES = {
|
||||
IKEA: {2200: 'efd275', 2700: 'f1e0b5', 4000: 'f5faf6'}
|
||||
}
|
||||
ALLOWED_TEMPERATURES = {IKEA}
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
|
@ -30,24 +29,26 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
return
|
||||
|
||||
gateway_id = discovery_info['gateway']
|
||||
api = hass.data[KEY_API][gateway_id]
|
||||
gateway = hass.data[KEY_GATEWAY][gateway_id]
|
||||
devices = gateway.get_devices()
|
||||
lights = [dev for dev in devices if dev.has_light_control]
|
||||
add_devices(Tradfri(light) for light in lights)
|
||||
devices = api(gateway.get_devices())
|
||||
lights = [dev for dev in devices if api(dev).has_light_control]
|
||||
add_devices(Tradfri(light, api) for light in lights)
|
||||
|
||||
allow_tradfri_groups = hass.data[KEY_TRADFRI_GROUPS][gateway_id]
|
||||
if allow_tradfri_groups:
|
||||
groups = gateway.get_groups()
|
||||
add_devices(TradfriGroup(group) for group in groups)
|
||||
groups = api(gateway.get_groups())
|
||||
add_devices(TradfriGroup(group, api) for group in groups)
|
||||
|
||||
|
||||
class TradfriGroup(Light):
|
||||
"""The platform class required by hass."""
|
||||
|
||||
def __init__(self, light):
|
||||
def __init__(self, light, api):
|
||||
"""Initialize a Group."""
|
||||
self._group = light
|
||||
self._name = light.name
|
||||
self._group = api(light)
|
||||
self._api = api
|
||||
self._name = self._group.name
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
|
@ -71,20 +72,20 @@ class TradfriGroup(Light):
|
|||
|
||||
def turn_off(self, **kwargs):
|
||||
"""Instruct the group lights to turn off."""
|
||||
self._group.set_state(0)
|
||||
self._api(self._group.set_state(0))
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""Instruct the group lights to turn on, or dim."""
|
||||
if ATTR_BRIGHTNESS in kwargs:
|
||||
self._group.set_dimmer(kwargs[ATTR_BRIGHTNESS])
|
||||
self._api(self._group.set_dimmer(kwargs[ATTR_BRIGHTNESS]))
|
||||
else:
|
||||
self._group.set_state(1)
|
||||
self._api(self._group.set_state(1))
|
||||
|
||||
def update(self):
|
||||
"""Fetch new state data for this group."""
|
||||
from pytradfri import RequestTimeout
|
||||
try:
|
||||
self._group.update()
|
||||
self._api(self._group.update())
|
||||
except RequestTimeout:
|
||||
_LOGGER.warning("Tradfri update request timed out")
|
||||
|
||||
|
@ -92,14 +93,15 @@ class TradfriGroup(Light):
|
|||
class Tradfri(Light):
|
||||
"""The platform class required by Home Asisstant."""
|
||||
|
||||
def __init__(self, light):
|
||||
def __init__(self, light, api):
|
||||
"""Initialize a Light."""
|
||||
self._light = light
|
||||
self._light = api(light)
|
||||
self._api = api
|
||||
|
||||
# Caching of LightControl and light object
|
||||
self._light_control = light.light_control
|
||||
self._light_data = light.light_control.lights[0]
|
||||
self._name = light.name
|
||||
self._light_control = self._light.light_control
|
||||
self._light_data = self._light_control.lights[0]
|
||||
self._name = self._light.name
|
||||
self._rgb_color = None
|
||||
self._features = SUPPORT_BRIGHTNESS
|
||||
|
||||
|
@ -109,8 +111,20 @@ class Tradfri(Light):
|
|||
else:
|
||||
self._features |= SUPPORT_RGB_COLOR
|
||||
|
||||
self._ok_temps = ALLOWED_TEMPERATURES.get(
|
||||
self._light.device_info.manufacturer)
|
||||
self._ok_temps = \
|
||||
self._light.device_info.manufacturer in ALLOWED_TEMPERATURES
|
||||
|
||||
@property
|
||||
def min_mireds(self):
|
||||
"""Return the coldest color_temp that this light supports."""
|
||||
from pytradfri.color import MAX_KELVIN_WS
|
||||
return color_util.color_temperature_kelvin_to_mired(MAX_KELVIN_WS)
|
||||
|
||||
@property
|
||||
def max_mireds(self):
|
||||
"""Return the warmest color_temp that this light supports."""
|
||||
from pytradfri.color import MIN_KELVIN_WS
|
||||
return color_util.color_temperature_kelvin_to_mired(MIN_KELVIN_WS)
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
|
@ -135,20 +149,13 @@ class Tradfri(Light):
|
|||
@property
|
||||
def color_temp(self):
|
||||
"""Return the CT color value in mireds."""
|
||||
if (self._light_data.hex_color is None or
|
||||
if (self._light_data.kelvin_color is None or
|
||||
self.supported_features & SUPPORT_COLOR_TEMP == 0 or
|
||||
not self._ok_temps):
|
||||
return None
|
||||
|
||||
kelvin = next((
|
||||
kelvin for kelvin, hex_color in self._ok_temps.items()
|
||||
if hex_color == self._light_data.hex_color), None)
|
||||
if kelvin is None:
|
||||
_LOGGER.error(
|
||||
"Unexpected color temperature found for %s: %s",
|
||||
self.name, self._light_data.hex_color)
|
||||
return
|
||||
return color_util.color_temperature_kelvin_to_mired(kelvin)
|
||||
return color_util.color_temperature_kelvin_to_mired(
|
||||
self._light_data.kelvin_color
|
||||
)
|
||||
|
||||
@property
|
||||
def rgb_color(self):
|
||||
|
@ -157,7 +164,7 @@ class Tradfri(Light):
|
|||
|
||||
def turn_off(self, **kwargs):
|
||||
"""Instruct the light to turn off."""
|
||||
self._light_control.set_state(False)
|
||||
self._api(self._light_control.set_state(False))
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
"""
|
||||
|
@ -167,29 +174,27 @@ class Tradfri(Light):
|
|||
for ATTR_RGB_COLOR, this also supports Philips Hue bulbs.
|
||||
"""
|
||||
if ATTR_BRIGHTNESS in kwargs:
|
||||
self._light_control.set_dimmer(kwargs[ATTR_BRIGHTNESS])
|
||||
self._api(self._light_control.set_dimmer(kwargs[ATTR_BRIGHTNESS]))
|
||||
else:
|
||||
self._light_control.set_state(True)
|
||||
self._api(self._light_control.set_state(True))
|
||||
|
||||
if ATTR_RGB_COLOR in kwargs and self._light_data.hex_color is not None:
|
||||
self._light.light_control.set_hex_color(
|
||||
color_util.color_rgb_to_hex(*kwargs[ATTR_RGB_COLOR]))
|
||||
self._api(self._light.light_control.set_hex_color(
|
||||
color_util.color_rgb_to_hex(*kwargs[ATTR_RGB_COLOR])))
|
||||
|
||||
elif ATTR_COLOR_TEMP in kwargs and \
|
||||
self._light_data.hex_color is not None and self._ok_temps:
|
||||
kelvin = color_util.color_temperature_mired_to_kelvin(
|
||||
kwargs[ATTR_COLOR_TEMP])
|
||||
# find closest allowed kelvin temp from user input
|
||||
kelvin = min(self._ok_temps.keys(), key=lambda x: abs(x - kelvin))
|
||||
self._light_control.set_hex_color(self._ok_temps[kelvin])
|
||||
self._api(self._light_control.set_kelvin_color(kelvin))
|
||||
|
||||
def update(self):
|
||||
"""Fetch new state data for this light."""
|
||||
from pytradfri import RequestTimeout
|
||||
try:
|
||||
self._light.update()
|
||||
except RequestTimeout:
|
||||
_LOGGER.warning("Tradfri update request timed out")
|
||||
self._api(self._light.update())
|
||||
except RequestTimeout as exception:
|
||||
_LOGGER.warning("Tradfri update request timed out: %s", exception)
|
||||
|
||||
# Handle Hue lights paired with the gateway
|
||||
# hex_color is 0 when bulb is unreachable
|
||||
|
|
227
homeassistant/components/light/xiaomi_philipslight.py
Normal file
227
homeassistant/components/light/xiaomi_philipslight.py
Normal file
|
@ -0,0 +1,227 @@
|
|||
"""
|
||||
Support for Xiaomi Philips Lights (LED Ball & Ceil).
|
||||
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/light.xiaomi_philipslight/
|
||||
"""
|
||||
import asyncio
|
||||
from functools import partial
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.light import (
|
||||
PLATFORM_SCHEMA, ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS,
|
||||
ATTR_COLOR_TEMP, SUPPORT_COLOR_TEMP, Light, )
|
||||
|
||||
from homeassistant.const import (CONF_NAME, CONF_HOST, CONF_TOKEN, )
|
||||
from homeassistant.exceptions import PlatformNotReady
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = 'Xiaomi Philips Light'
|
||||
PLATFORM = 'xiaomi_philipslight'
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_TOKEN): vol.All(cv.string, vol.Length(min=32, max=32)),
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
})
|
||||
|
||||
REQUIREMENTS = ['python-mirobo==0.1.3']
|
||||
|
||||
# The light does not accept cct values < 1
|
||||
CCT_MIN = 1
|
||||
CCT_MAX = 100
|
||||
|
||||
SUCCESS = ['ok']
|
||||
ATTR_MODEL = 'model'
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set up the light from config."""
|
||||
from mirobo import Ceil, DeviceException
|
||||
if PLATFORM not in hass.data:
|
||||
hass.data[PLATFORM] = {}
|
||||
|
||||
host = config.get(CONF_HOST)
|
||||
name = config.get(CONF_NAME)
|
||||
token = config.get(CONF_TOKEN)
|
||||
|
||||
_LOGGER.info("Initializing with host %s (token %s...)", host, token[:5])
|
||||
|
||||
try:
|
||||
light = Ceil(host, token)
|
||||
device_info = light.info()
|
||||
_LOGGER.info("%s %s %s initialized",
|
||||
device_info.raw['model'],
|
||||
device_info.raw['fw_ver'],
|
||||
device_info.raw['hw_ver'])
|
||||
|
||||
philips_light = XiaomiPhilipsLight(name, light, device_info)
|
||||
hass.data[PLATFORM][host] = philips_light
|
||||
except DeviceException:
|
||||
raise PlatformNotReady
|
||||
|
||||
async_add_devices([philips_light], update_before_add=True)
|
||||
|
||||
|
||||
class XiaomiPhilipsLight(Light):
|
||||
"""Representation of a Xiaomi Philips Light."""
|
||||
|
||||
def __init__(self, name, light, device_info):
|
||||
"""Initialize the light device."""
|
||||
self._name = name
|
||||
self._device_info = device_info
|
||||
|
||||
self._brightness = None
|
||||
self._color_temp = None
|
||||
|
||||
self._light = light
|
||||
self._state = None
|
||||
self._state_attrs = {
|
||||
ATTR_MODEL: self._device_info.raw['model'],
|
||||
}
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Poll the light."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device if any."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return true when state is known."""
|
||||
return self._state is not None
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes of the device."""
|
||||
return self._state_attrs
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if light is on."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
"""Return the brightness of this light between 0..255."""
|
||||
return self._brightness
|
||||
|
||||
@property
|
||||
def color_temp(self):
|
||||
"""Return the color temperature."""
|
||||
return self._color_temp
|
||||
|
||||
@property
|
||||
def min_mireds(self):
|
||||
"""Return the coldest color_temp that this light supports."""
|
||||
return 175
|
||||
|
||||
@property
|
||||
def max_mireds(self):
|
||||
"""Return the warmest color_temp that this light supports."""
|
||||
return 333
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Return the supported features."""
|
||||
return SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP
|
||||
|
||||
@asyncio.coroutine
|
||||
def _try_command(self, mask_error, func, *args, **kwargs):
|
||||
"""Call a light command handling error messages."""
|
||||
from mirobo import DeviceException
|
||||
try:
|
||||
result = yield from self.hass.async_add_job(
|
||||
partial(func, *args, **kwargs))
|
||||
|
||||
_LOGGER.debug("Response received from light: %s", result)
|
||||
|
||||
return result == SUCCESS
|
||||
except DeviceException as exc:
|
||||
_LOGGER.error(mask_error, exc)
|
||||
return False
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_turn_on(self, **kwargs):
|
||||
"""Turn the light on."""
|
||||
if ATTR_BRIGHTNESS in kwargs:
|
||||
brightness = kwargs[ATTR_BRIGHTNESS]
|
||||
percent_brightness = int(100 * brightness / 255)
|
||||
|
||||
_LOGGER.debug(
|
||||
"Setting brightness: %s %s%%",
|
||||
self.brightness, percent_brightness)
|
||||
|
||||
result = yield from self._try_command(
|
||||
"Setting brightness failed: %s",
|
||||
self._light.set_bright, percent_brightness)
|
||||
|
||||
if result:
|
||||
self._brightness = brightness
|
||||
|
||||
if ATTR_COLOR_TEMP in kwargs:
|
||||
color_temp = kwargs[ATTR_COLOR_TEMP]
|
||||
percent_color_temp = self.translate(
|
||||
color_temp, self.max_mireds,
|
||||
self.min_mireds, CCT_MIN, CCT_MAX)
|
||||
|
||||
_LOGGER.debug(
|
||||
"Setting color temperature: "
|
||||
"%s mireds, %s%% cct",
|
||||
color_temp, percent_color_temp)
|
||||
|
||||
result = yield from self._try_command(
|
||||
"Setting color temperature failed: %s cct",
|
||||
self._light.set_cct, percent_color_temp)
|
||||
|
||||
if result:
|
||||
self._color_temp = color_temp
|
||||
|
||||
result = yield from self._try_command(
|
||||
"Turning the light on failed.", self._light.on)
|
||||
|
||||
if result:
|
||||
self._state = True
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_turn_off(self, **kwargs):
|
||||
"""Turn the light off."""
|
||||
result = yield from self._try_command(
|
||||
"Turning the light off failed.", self._light.off)
|
||||
|
||||
if result:
|
||||
self._state = True
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
"""Fetch state from the device."""
|
||||
from mirobo import DeviceException
|
||||
try:
|
||||
state = yield from self.hass.async_add_job(self._light.status)
|
||||
_LOGGER.debug("Got new state: %s", state.data)
|
||||
|
||||
self._state = state.is_on
|
||||
self._brightness = int(255 * 0.01 * state.bright)
|
||||
self._color_temp = self.translate(state.cct, CCT_MIN, CCT_MAX,
|
||||
self.max_mireds,
|
||||
self.min_mireds)
|
||||
|
||||
except DeviceException as ex:
|
||||
_LOGGER.error("Got exception while fetching the state: %s", ex)
|
||||
|
||||
@staticmethod
|
||||
def translate(value, left_min, left_max, right_min, right_max):
|
||||
"""Map a value from left span to right span."""
|
||||
left_span = left_max - left_min
|
||||
right_span = right_max - right_min
|
||||
value_scaled = float(value - left_min) / float(left_span)
|
||||
return int(right_min + (value_scaled * right_span))
|
|
@ -27,8 +27,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
|||
|
||||
endpoint = discovery_info['endpoint']
|
||||
try:
|
||||
primaries = yield from endpoint.light_color['num_primaries']
|
||||
discovery_info['num_primaries'] = primaries
|
||||
discovery_info['color_capabilities'] \
|
||||
= yield from endpoint.light_color['color_capabilities']
|
||||
except (AttributeError, KeyError):
|
||||
pass
|
||||
|
||||
|
@ -54,11 +54,11 @@ class Light(zha.Entity, light.Light):
|
|||
self._supported_features |= light.SUPPORT_TRANSITION
|
||||
self._brightness = 0
|
||||
if zcl_clusters.lighting.Color.cluster_id in self._in_clusters:
|
||||
# Not sure all color lights necessarily support this directly
|
||||
# Should we emulate it?
|
||||
self._supported_features |= light.SUPPORT_COLOR_TEMP
|
||||
# Silly heuristic, not sure if it works widely
|
||||
if kwargs.get('num_primaries', 1) >= 3:
|
||||
color_capabilities = kwargs.get('color_capabilities', 0x10)
|
||||
if color_capabilities & 0x10:
|
||||
self._supported_features |= light.SUPPORT_COLOR_TEMP
|
||||
|
||||
if color_capabilities & 0x08:
|
||||
self._supported_features |= light.SUPPORT_XY_COLOR
|
||||
self._supported_features |= light.SUPPORT_RGB_COLOR
|
||||
self._xy_color = (1.0, 1.0)
|
||||
|
|
49
homeassistant/components/lock/abode.py
Normal file
49
homeassistant/components/lock/abode.py
Normal file
|
@ -0,0 +1,49 @@
|
|||
"""
|
||||
This component provides HA lock support for Abode Security System.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/lock.abode/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.abode import AbodeDevice, DATA_ABODE
|
||||
from homeassistant.components.lock import LockDevice
|
||||
|
||||
|
||||
DEPENDENCIES = ['abode']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up Abode lock devices."""
|
||||
import abodepy.helpers.constants as CONST
|
||||
|
||||
abode = hass.data[DATA_ABODE]
|
||||
|
||||
sensors = []
|
||||
for sensor in abode.get_devices(type_filter=(CONST.DEVICE_DOOR_LOCK)):
|
||||
sensors.append(AbodeLock(abode, sensor))
|
||||
|
||||
add_devices(sensors)
|
||||
|
||||
|
||||
class AbodeLock(AbodeDevice, LockDevice):
|
||||
"""Representation of an Abode lock."""
|
||||
|
||||
def __init__(self, controller, device):
|
||||
"""Initialize the Abode device."""
|
||||
AbodeDevice.__init__(self, controller, device)
|
||||
|
||||
def lock(self, **kwargs):
|
||||
"""Lock the device."""
|
||||
self._device.lock()
|
||||
|
||||
def unlock(self, **kwargs):
|
||||
"""Unlock the device."""
|
||||
self._device.unlock()
|
||||
|
||||
@property
|
||||
def is_locked(self):
|
||||
"""Return true if device is on."""
|
||||
return self._device.is_locked
|
|
@ -13,7 +13,7 @@ import homeassistant.helpers.config_validation as cv
|
|||
from homeassistant.components.lock import (LockDevice, PLATFORM_SCHEMA)
|
||||
from homeassistant.const import (CONF_PASSWORD, CONF_USERNAME)
|
||||
|
||||
REQUIREMENTS = ['pynello==1.5']
|
||||
REQUIREMENTS = ['pynello==1.5.1']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
|
57
homeassistant/components/lock/tesla.py
Normal file
57
homeassistant/components/lock/tesla.py
Normal file
|
@ -0,0 +1,57 @@
|
|||
"""
|
||||
Support for Tesla door locks.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/lock.tesla/
|
||||
"""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.lock import ENTITY_ID_FORMAT, LockDevice
|
||||
from homeassistant.components.tesla import DOMAIN as TESLA_DOMAIN, TeslaDevice
|
||||
from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['tesla']
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Tesla lock platform."""
|
||||
devices = [TeslaLock(device, hass.data[TESLA_DOMAIN]['controller'])
|
||||
for device in hass.data[TESLA_DOMAIN]['devices']['lock']]
|
||||
add_devices(devices, True)
|
||||
|
||||
|
||||
class TeslaLock(TeslaDevice, LockDevice):
|
||||
"""Representation of a Tesla door lock."""
|
||||
|
||||
def __init__(self, tesla_device, controller):
|
||||
"""Initialisation of the lock."""
|
||||
self._state = None
|
||||
super().__init__(tesla_device, controller)
|
||||
self._name = self.tesla_device.name
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(self.tesla_id)
|
||||
|
||||
def lock(self, **kwargs):
|
||||
"""Send the lock command."""
|
||||
_LOGGER.debug("Locking doors for: %s", self._name)
|
||||
self.tesla_device.lock()
|
||||
self._state = STATE_LOCKED
|
||||
|
||||
def unlock(self, **kwargs):
|
||||
"""Send the unlock command."""
|
||||
_LOGGER.debug("Unlocking doors for: %s", self._name)
|
||||
self.tesla_device.unlock()
|
||||
self._state = STATE_UNLOCKED
|
||||
|
||||
@property
|
||||
def is_locked(self):
|
||||
"""Get whether the lock is in locked state."""
|
||||
return self._state == STATE_LOCKED
|
||||
|
||||
def update(self):
|
||||
"""Updating state of the lock."""
|
||||
_LOGGER.debug("Updating state for: %s", self._name)
|
||||
self.tesla_device.update()
|
||||
self._state = STATE_LOCKED if self.tesla_device.is_locked() \
|
||||
else STATE_UNLOCKED
|
|
@ -14,7 +14,7 @@ from homeassistant.const import CONF_HOST
|
|||
from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
REQUIREMENTS = ['pylutron-caseta==0.2.7']
|
||||
REQUIREMENTS = ['pylutron-caseta==0.2.8']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ from homeassistant.components.media_player import (
|
|||
from homeassistant.config import load_yaml_config_file
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['youtube_dl==2017.8.18']
|
||||
REQUIREMENTS = ['youtube_dl==2017.9.2']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
@ -17,15 +17,16 @@ from homeassistant.components.media_player import (
|
|||
MEDIA_TYPE_MUSIC, SUPPORT_VOLUME_SET, SUPPORT_PLAY)
|
||||
from homeassistant.const import (
|
||||
CONF_HOST, STATE_OFF, STATE_PLAYING, STATE_PAUSED,
|
||||
CONF_NAME, STATE_ON, CONF_ZONE)
|
||||
CONF_NAME, STATE_ON, CONF_ZONE, CONF_TIMEOUT)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['denonavr==0.5.2']
|
||||
REQUIREMENTS = ['denonavr==0.5.3']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = None
|
||||
DEFAULT_SHOW_SOURCES = False
|
||||
DEFAULT_TIMEOUT = 2
|
||||
CONF_SHOW_ALL_SOURCES = 'show_all_sources'
|
||||
CONF_ZONES = 'zones'
|
||||
CONF_VALID_ZONES = ['Zone2', 'Zone3']
|
||||
|
@ -51,7 +52,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||
vol.Optional(CONF_SHOW_ALL_SOURCES, default=DEFAULT_SHOW_SOURCES):
|
||||
cv.boolean,
|
||||
vol.Optional(CONF_ZONES):
|
||||
vol.All(cv.ensure_list, [DENON_ZONE_SCHEMA])
|
||||
vol.All(cv.ensure_list, [DENON_ZONE_SCHEMA]),
|
||||
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
|
||||
})
|
||||
|
||||
NewHost = namedtuple('NewHost', ['host', 'name'])
|
||||
|
@ -69,8 +71,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
if cache is None:
|
||||
cache = hass.data[KEY_DENON_CACHE] = set()
|
||||
|
||||
# Get config option for show_all_sources
|
||||
# Get config option for show_all_sources and timeout
|
||||
show_all_sources = config.get(CONF_SHOW_ALL_SOURCES)
|
||||
timeout = config.get(CONF_TIMEOUT)
|
||||
|
||||
# Get config option for additional zones
|
||||
zones = config.get(CONF_ZONES)
|
||||
|
@ -103,14 +106,17 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
for d_receiver in d_receivers:
|
||||
host = d_receiver["host"]
|
||||
name = d_receiver["friendlyName"]
|
||||
new_hosts.append(NewHost(host=host, name=name))
|
||||
new_hosts.append(
|
||||
NewHost(host=host, name=name))
|
||||
|
||||
for entry in new_hosts:
|
||||
# Check if host not in cache, append it and save for later
|
||||
# starting
|
||||
if entry.host not in cache:
|
||||
new_device = denonavr.DenonAVR(
|
||||
entry.host, entry.name, show_all_sources, add_zones)
|
||||
host=entry.host, name=entry.name,
|
||||
show_all_inputs=show_all_sources, timeout=timeout,
|
||||
add_zones=add_zones)
|
||||
for new_zone in new_device.zones.values():
|
||||
receivers.append(DenonDevice(new_zone))
|
||||
cache.add(host)
|
||||
|
|
|
@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at
|
|||
https://home-assistant.io/components/media_player.directv/
|
||||
"""
|
||||
import voluptuous as vol
|
||||
import requests
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA,
|
||||
|
@ -25,7 +26,7 @@ SUPPORT_DTV = SUPPORT_PAUSE | SUPPORT_TURN_ON | SUPPORT_TURN_OFF | \
|
|||
SUPPORT_PLAY_MEDIA | SUPPORT_STOP | SUPPORT_NEXT_TRACK | \
|
||||
SUPPORT_PREVIOUS_TRACK | SUPPORT_PLAY
|
||||
|
||||
KNOWN_HOSTS = []
|
||||
DATA_DIRECTV = "data_directv"
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
|
@ -37,32 +38,45 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
|||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the DirecTV platform."""
|
||||
known_devices = hass.data.get(DATA_DIRECTV)
|
||||
if not known_devices:
|
||||
known_devices = []
|
||||
hosts = []
|
||||
|
||||
if discovery_info:
|
||||
host = discovery_info.get('host')
|
||||
|
||||
if host in KNOWN_HOSTS:
|
||||
return
|
||||
|
||||
hosts.append([
|
||||
'DirecTV_' + discovery_info.get('serial', ''),
|
||||
host, DEFAULT_PORT
|
||||
])
|
||||
|
||||
elif CONF_HOST in config:
|
||||
if CONF_HOST in config:
|
||||
hosts.append([
|
||||
config.get(CONF_NAME), config.get(CONF_HOST),
|
||||
config.get(CONF_PORT), config.get(CONF_DEVICE)
|
||||
])
|
||||
|
||||
elif discovery_info:
|
||||
host = discovery_info.get('host')
|
||||
name = 'DirecTV_' + discovery_info.get('serial', '')
|
||||
|
||||
# attempt to discover additional RVU units
|
||||
try:
|
||||
resp = requests.get(
|
||||
'http://%s:%d/info/getLocations' % (host, DEFAULT_PORT)).json()
|
||||
if "locations" in resp:
|
||||
for loc in resp["locations"]:
|
||||
if("locationName" in loc and "clientAddr" in loc
|
||||
and loc["clientAddr"] not in known_devices):
|
||||
hosts.append([str.title(loc["locationName"]), host,
|
||||
DEFAULT_PORT, loc["clientAddr"]])
|
||||
|
||||
except requests.exceptions.RequestException:
|
||||
# bail out and just go forward with uPnP data
|
||||
if DEFAULT_DEVICE not in known_devices:
|
||||
hosts.append([name, host, DEFAULT_PORT, DEFAULT_DEVICE])
|
||||
|
||||
dtvs = []
|
||||
|
||||
for host in hosts:
|
||||
dtvs.append(DirecTvDevice(*host))
|
||||
KNOWN_HOSTS.append(host)
|
||||
known_devices.append(host[-1])
|
||||
|
||||
add_devices(dtvs)
|
||||
hass.data[DATA_DIRECTV] = known_devices
|
||||
|
||||
return True
|
||||
|
||||
|
|
|
@ -322,6 +322,7 @@ class SonosDevice(MediaPlayerDevice):
|
|||
self._media_title = None
|
||||
self._media_radio_show = None
|
||||
self._media_next_title = None
|
||||
self._available = True
|
||||
self._support_previous_track = False
|
||||
self._support_next_track = False
|
||||
self._support_play = False
|
||||
|
@ -386,6 +387,11 @@ class SonosDevice(MediaPlayerDevice):
|
|||
"""Return coordinator of this player."""
|
||||
return self._coordinator
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
return self._available
|
||||
|
||||
def _is_available(self):
|
||||
try:
|
||||
sock = socket.create_connection(
|
||||
|
@ -416,11 +422,11 @@ class SonosDevice(MediaPlayerDevice):
|
|||
self._player.get_sonos_favorites()['favorites']
|
||||
|
||||
if self._last_avtransport_event:
|
||||
is_available = True
|
||||
self._available = True
|
||||
else:
|
||||
is_available = self._is_available()
|
||||
self._available = self._is_available()
|
||||
|
||||
if not is_available:
|
||||
if not self._available:
|
||||
self._player_volume = None
|
||||
self._player_volume_muted = None
|
||||
self._status = 'OFF'
|
||||
|
@ -897,7 +903,8 @@ class SonosDevice(MediaPlayerDevice):
|
|||
src = fav.pop()
|
||||
self._source_name = src['title']
|
||||
|
||||
if 'object.container.playlistContainer' in src['meta']:
|
||||
if ('object.container.playlistContainer' in src['meta'] or
|
||||
'object.container.album.musicAlbum' in src['meta']):
|
||||
self._replace_queue_with_playlist(src)
|
||||
self._player.play_from_queue(0)
|
||||
else:
|
||||
|
|
|
@ -148,6 +148,10 @@ class SpotifyMediaPlayer(MediaPlayerDevice):
|
|||
new_token = \
|
||||
self._oauth.refresh_access_token(
|
||||
self._token_info['refresh_token'])
|
||||
# skip when refresh failed
|
||||
if new_token is None:
|
||||
return
|
||||
|
||||
self._token_info = new_token
|
||||
token_refreshed = True
|
||||
if self._player is None or token_refreshed:
|
||||
|
@ -158,6 +162,12 @@ class SpotifyMediaPlayer(MediaPlayerDevice):
|
|||
def update(self):
|
||||
"""Update state and attributes."""
|
||||
self.refresh_spotify_instance()
|
||||
|
||||
# Don't true update when token is expired
|
||||
if self._oauth.is_token_expired(self._token_info):
|
||||
_LOGGER.warning("Spotify failed to update, token expired.")
|
||||
return
|
||||
|
||||
# Available devices
|
||||
player_devices = self._player.devices()
|
||||
if player_devices is not None:
|
||||
|
|
233
homeassistant/components/media_player/yamaha_musiccast.py
Normal file
233
homeassistant/components/media_player/yamaha_musiccast.py
Normal file
|
@ -0,0 +1,233 @@
|
|||
"""Example for configuration.yaml.
|
||||
|
||||
media_player:
|
||||
- platform: yamaha_musiccast
|
||||
name: "Living Room"
|
||||
host: 192.168.xxx.xx
|
||||
port: 5005
|
||||
|
||||
"""
|
||||
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_NAME, CONF_HOST, CONF_PORT,
|
||||
STATE_UNKNOWN, STATE_ON
|
||||
)
|
||||
from homeassistant.components.media_player import (
|
||||
MediaPlayerDevice, MEDIA_TYPE_MUSIC, PLATFORM_SCHEMA,
|
||||
SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK,
|
||||
SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_PLAY,
|
||||
SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE,
|
||||
SUPPORT_SELECT_SOURCE, SUPPORT_STOP
|
||||
)
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SUPPORTED_FEATURES = (
|
||||
SUPPORT_PLAY | SUPPORT_PAUSE | SUPPORT_STOP |
|
||||
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK |
|
||||
SUPPORT_TURN_ON | SUPPORT_TURN_OFF |
|
||||
SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE |
|
||||
SUPPORT_SELECT_SOURCE
|
||||
)
|
||||
|
||||
REQUIREMENTS = ['pymusiccast==0.1.0']
|
||||
|
||||
DEFAULT_NAME = "Yamaha Receiver"
|
||||
DEFAULT_PORT = 5005
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.positive_int,
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the Yamaha MusicCast platform."""
|
||||
import pymusiccast
|
||||
|
||||
name = config.get(CONF_NAME)
|
||||
host = config.get(CONF_HOST)
|
||||
port = config.get(CONF_PORT)
|
||||
|
||||
receiver = pymusiccast.McDevice(host, udp_port=port)
|
||||
_LOGGER.debug("receiver: %s / Port: %d", receiver, port)
|
||||
|
||||
add_devices([YamahaDevice(receiver, name)], True)
|
||||
|
||||
|
||||
class YamahaDevice(MediaPlayerDevice):
|
||||
"""Representation of a Yamaha MusicCast device."""
|
||||
|
||||
def __init__(self, receiver, name):
|
||||
"""Initialize the Yamaha MusicCast device."""
|
||||
self._receiver = receiver
|
||||
self._name = name
|
||||
self.power = STATE_UNKNOWN
|
||||
self.volume = 0
|
||||
self.volume_max = 0
|
||||
self.mute = False
|
||||
self._source = None
|
||||
self._source_list = []
|
||||
self.status = STATE_UNKNOWN
|
||||
self.media_status = None
|
||||
self._receiver.set_yamaha_device(self)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
if self.power == STATE_ON and self.status is not STATE_UNKNOWN:
|
||||
return self.status
|
||||
return self.power
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Push an update after each command."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def is_volume_muted(self):
|
||||
"""Boolean if volume is currently muted."""
|
||||
return self.mute
|
||||
|
||||
@property
|
||||
def volume_level(self):
|
||||
"""Volume level of the media player (0..1)."""
|
||||
return self.volume
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag of features that are supported."""
|
||||
return SUPPORTED_FEATURES
|
||||
|
||||
@property
|
||||
def source(self):
|
||||
"""Return the current input source."""
|
||||
return self._source
|
||||
|
||||
@property
|
||||
def source_list(self):
|
||||
"""List of available input sources."""
|
||||
return self._source_list
|
||||
|
||||
@source_list.setter
|
||||
def source_list(self, value):
|
||||
"""Set source_list attribute."""
|
||||
self._source_list = value
|
||||
|
||||
@property
|
||||
def media_content_type(self):
|
||||
"""Return the media content type."""
|
||||
return MEDIA_TYPE_MUSIC
|
||||
|
||||
@property
|
||||
def media_duration(self):
|
||||
"""Duration of current playing media in seconds."""
|
||||
return self.media_status.media_duration \
|
||||
if self.media_status else None
|
||||
|
||||
@property
|
||||
def media_image_url(self):
|
||||
"""Image url of current playing media."""
|
||||
return self.media_status.media_image_url \
|
||||
if self.media_status else None
|
||||
|
||||
@property
|
||||
def media_artist(self):
|
||||
"""Artist of current playing media, music track only."""
|
||||
return self.media_status.media_artist if self.media_status else None
|
||||
|
||||
@property
|
||||
def media_album(self):
|
||||
"""Album of current playing media, music track only."""
|
||||
return self.media_status.media_album if self.media_status else None
|
||||
|
||||
@property
|
||||
def media_track(self):
|
||||
"""Track number of current playing media, music track only."""
|
||||
return self.media_status.media_track if self.media_status else None
|
||||
|
||||
@property
|
||||
def media_title(self):
|
||||
"""Title of current playing media."""
|
||||
return self.media_status.media_title if self.media_status else None
|
||||
|
||||
def update(self):
|
||||
"""Get the latest details from the device."""
|
||||
_LOGGER.debug("update: %s", self.entity_id)
|
||||
|
||||
# call from constructor setup_platform()
|
||||
if not self.entity_id:
|
||||
_LOGGER.debug("First run")
|
||||
self._receiver.update_status(push=False)
|
||||
# call from regular polling
|
||||
else:
|
||||
# update_status_timer was set before
|
||||
if self._receiver.update_status_timer:
|
||||
_LOGGER.debug(
|
||||
"is_alive: %s",
|
||||
self._receiver.update_status_timer.is_alive())
|
||||
# e.g. computer was suspended, while hass was running
|
||||
if not self._receiver.update_status_timer.is_alive():
|
||||
_LOGGER.debug("Reinitializing")
|
||||
self._receiver.update_status()
|
||||
|
||||
def turn_on(self):
|
||||
"""Turn on specified media player or all."""
|
||||
_LOGGER.debug("Turn device: on")
|
||||
self._receiver.set_power(True)
|
||||
|
||||
def turn_off(self):
|
||||
"""Turn off specified media player or all."""
|
||||
_LOGGER.debug("Turn device: off")
|
||||
self._receiver.set_power(False)
|
||||
|
||||
def media_play(self):
|
||||
"""Send the media player the command for play/pause."""
|
||||
_LOGGER.debug("Play")
|
||||
self._receiver.set_playback("play")
|
||||
|
||||
def media_pause(self):
|
||||
"""Send the media player the command for pause."""
|
||||
_LOGGER.debug("Pause")
|
||||
self._receiver.set_playback("pause")
|
||||
|
||||
def media_stop(self):
|
||||
"""Send the media player the stop command."""
|
||||
_LOGGER.debug("Stop")
|
||||
self._receiver.set_playback("stop")
|
||||
|
||||
def media_previous_track(self):
|
||||
"""Send the media player the command for prev track."""
|
||||
_LOGGER.debug("Previous")
|
||||
self._receiver.set_playback("previous")
|
||||
|
||||
def media_next_track(self):
|
||||
"""Send the media player the command for next track."""
|
||||
_LOGGER.debug("Next")
|
||||
self._receiver.set_playback("next")
|
||||
|
||||
def mute_volume(self, mute):
|
||||
"""Send mute command."""
|
||||
_LOGGER.debug("Mute volume: %s", mute)
|
||||
self._receiver.set_mute(mute)
|
||||
|
||||
def set_volume_level(self, volume):
|
||||
"""Set volume level, range 0..1."""
|
||||
_LOGGER.debug("Volume level: %.2f / %d",
|
||||
volume, volume * self.volume_max)
|
||||
self._receiver.set_volume(volume * self.volume_max)
|
||||
|
||||
def select_source(self, source):
|
||||
"""Send the media player the command to select input source."""
|
||||
_LOGGER.debug("select_source: %s", source)
|
||||
self.status = STATE_UNKNOWN
|
||||
self._receiver.set_input(source)
|
35
homeassistant/components/mycroft.py
Normal file
35
homeassistant/components/mycroft.py
Normal file
|
@ -0,0 +1,35 @@
|
|||
"""
|
||||
Support for Mycroft AI.
|
||||
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/mycroft
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.helpers import discovery
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['mycroftapi==2.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
DOMAIN = 'mycroft'
|
||||
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_HOST): cv.string
|
||||
})
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Set up the Mycroft component."""
|
||||
hass.data[DOMAIN] = config[DOMAIN][CONF_HOST]
|
||||
discovery.load_platform(hass, 'notify', DOMAIN, {}, config)
|
||||
return True
|
|
@ -27,7 +27,7 @@ from homeassistant.helpers.entity import Entity
|
|||
from homeassistant.loader import get_component
|
||||
from homeassistant.setup import setup_component
|
||||
|
||||
REQUIREMENTS = ['pymysensors==0.11.0']
|
||||
REQUIREMENTS = ['pymysensors==0.11.1']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -49,6 +49,9 @@ CONF_TOPIC_IN_PREFIX = 'topic_in_prefix'
|
|||
CONF_TOPIC_OUT_PREFIX = 'topic_out_prefix'
|
||||
CONF_VERSION = 'version'
|
||||
|
||||
CONF_NODES = 'nodes'
|
||||
CONF_NODE_NAME = 'name'
|
||||
|
||||
DEFAULT_BAUD_RATE = 115200
|
||||
DEFAULT_TCP_PORT = 5003
|
||||
DEFAULT_VERSION = '1.4'
|
||||
|
@ -132,6 +135,12 @@ def deprecated(key):
|
|||
return validator
|
||||
|
||||
|
||||
NODE_SCHEMA = vol.Schema({
|
||||
cv.positive_int: {
|
||||
vol.Required(CONF_NODE_NAME): cv.string
|
||||
}
|
||||
})
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema(vol.All(deprecated(CONF_DEBUG), {
|
||||
vol.Required(CONF_GATEWAYS): vol.All(
|
||||
|
@ -151,6 +160,7 @@ CONFIG_SCHEMA = vol.Schema({
|
|||
CONF_TOPIC_IN_PREFIX, default=''): valid_subscribe_topic,
|
||||
vol.Optional(
|
||||
CONF_TOPIC_OUT_PREFIX, default=''): valid_publish_topic,
|
||||
vol.Optional(CONF_NODES, default={}): NODE_SCHEMA,
|
||||
}]
|
||||
),
|
||||
vol.Optional(CONF_OPTIMISTIC, default=False): cv.boolean,
|
||||
|
@ -358,6 +368,7 @@ def setup(hass, config):
|
|||
device, persistence_file, baud_rate, tcp_port, in_prefix,
|
||||
out_prefix)
|
||||
if ready_gateway is not None:
|
||||
ready_gateway.nodes_config = gway.get(CONF_NODES)
|
||||
gateways[id(ready_gateway)] = ready_gateway
|
||||
|
||||
if not gateways:
|
||||
|
@ -474,12 +485,14 @@ def gw_callback_factory(hass):
|
|||
validated = validate_child(msg.gateway, msg.node_id, child)
|
||||
for platform, dev_ids in validated.items():
|
||||
devices = get_mysensors_devices(hass, platform)
|
||||
for idx, dev_id in enumerate(list(dev_ids)):
|
||||
new_dev_ids = []
|
||||
for dev_id in dev_ids:
|
||||
if dev_id in devices:
|
||||
dev_ids.pop(idx)
|
||||
signals.append(SIGNAL_CALLBACK.format(*dev_id))
|
||||
if dev_ids:
|
||||
discover_mysensors_platform(hass, platform, dev_ids)
|
||||
else:
|
||||
new_dev_ids.append(dev_id)
|
||||
if new_dev_ids:
|
||||
discover_mysensors_platform(hass, platform, new_dev_ids)
|
||||
for signal in set(signals):
|
||||
# Only one signal per device is needed.
|
||||
# A device can have multiple platforms, ie multiple schemas.
|
||||
|
@ -495,8 +508,13 @@ def gw_callback_factory(hass):
|
|||
|
||||
def get_mysensors_name(gateway, node_id, child_id):
|
||||
"""Return a name for a node child."""
|
||||
return '{} {} {}'.format(
|
||||
gateway.sensors[node_id].sketch_name, node_id, child_id)
|
||||
node_name = '{} {}'.format(
|
||||
gateway.sensors[node_id].sketch_name, node_id)
|
||||
node_name = next(
|
||||
(node[CONF_NODE_NAME] for conf_id, node in gateway.nodes_config.items()
|
||||
if node.get(CONF_NODE_NAME) is not None and conf_id == node_id),
|
||||
node_name)
|
||||
return '{} {}'.format(node_name, child_id)
|
||||
|
||||
|
||||
def get_mysensors_gateway(hass, gateway_id):
|
||||
|
|
|
@ -82,8 +82,6 @@ def async_setup(hass, config):
|
|||
"""Set up a notify platform."""
|
||||
if p_config is None:
|
||||
p_config = {}
|
||||
if discovery_info is None:
|
||||
discovery_info = {}
|
||||
|
||||
platform = yield from async_prepare_setup_platform(
|
||||
hass, config, DOMAIN, p_type)
|
||||
|
@ -105,8 +103,12 @@ def async_setup(hass, config):
|
|||
raise HomeAssistantError("Invalid notify platform.")
|
||||
|
||||
if notify_service is None:
|
||||
_LOGGER.error(
|
||||
"Failed to initialize notification service %s", p_type)
|
||||
# Platforms can decide not to create a service based
|
||||
# on discovery data.
|
||||
if discovery_info is None:
|
||||
_LOGGER.error(
|
||||
"Failed to initialize notification service %s",
|
||||
p_type)
|
||||
return
|
||||
|
||||
except Exception: # pylint: disable=broad-except
|
||||
|
@ -115,6 +117,9 @@ def async_setup(hass, config):
|
|||
|
||||
notify_service.hass = hass
|
||||
|
||||
if discovery_info is None:
|
||||
discovery_info = {}
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_notify_message(service):
|
||||
"""Handle sending notification message service calls."""
|
||||
|
|
|
@ -15,7 +15,7 @@ from homeassistant.components.notify import (
|
|||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
REQUIREMENTS = ['discord.py==0.16.10']
|
||||
REQUIREMENTS = ['discord.py==0.16.11']
|
||||
|
||||
CONF_TOKEN = 'token'
|
||||
|
||||
|
|
99
homeassistant/components/notify/knx.py
Normal file
99
homeassistant/components/notify/knx.py
Normal file
|
@ -0,0 +1,99 @@
|
|||
"""
|
||||
KNX/IP notification service.
|
||||
|
||||
For more details about this platform, please refer to the documentation
|
||||
https://home-assistant.io/components/notify.knx/
|
||||
"""
|
||||
import asyncio
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.knx import DATA_KNX, ATTR_DISCOVER_DEVICES
|
||||
from homeassistant.components.notify import PLATFORM_SCHEMA, \
|
||||
BaseNotificationService
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
CONF_ADDRESS = 'address'
|
||||
DEFAULT_NAME = 'KNX Notify'
|
||||
DEPENDENCIES = ['knx']
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_ADDRESS): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_get_service(hass, config, discovery_info=None):
|
||||
"""Get the KNX notification service."""
|
||||
if DATA_KNX not in hass.data \
|
||||
or not hass.data[DATA_KNX].initialized:
|
||||
return False
|
||||
|
||||
return async_get_service_discovery(hass, discovery_info) \
|
||||
if discovery_info is not None else \
|
||||
async_get_service_config(hass, config)
|
||||
|
||||
|
||||
@callback
|
||||
def async_get_service_discovery(hass, discovery_info):
|
||||
"""Set up notifications for KNX platform configured via xknx.yaml."""
|
||||
notification_devices = []
|
||||
for device_name in discovery_info[ATTR_DISCOVER_DEVICES]:
|
||||
device = hass.data[DATA_KNX].xknx.devices[device_name]
|
||||
notification_devices.append(device)
|
||||
return \
|
||||
KNXNotificationService(hass, notification_devices) \
|
||||
if notification_devices else \
|
||||
None
|
||||
|
||||
|
||||
@callback
|
||||
def async_get_service_config(hass, config):
|
||||
"""Set up notification for KNX platform configured within plattform."""
|
||||
import xknx
|
||||
notification = xknx.devices.Notification(
|
||||
hass.data[DATA_KNX].xknx,
|
||||
name=config.get(CONF_NAME),
|
||||
group_address=config.get(CONF_ADDRESS))
|
||||
hass.data[DATA_KNX].xknx.devices.add(notification)
|
||||
return KNXNotificationService(hass, [notification, ])
|
||||
|
||||
|
||||
class KNXNotificationService(BaseNotificationService):
|
||||
"""Implement demo notification service."""
|
||||
|
||||
def __init__(self, hass, devices):
|
||||
"""Initialize the service."""
|
||||
self.hass = hass
|
||||
self.devices = devices
|
||||
|
||||
@property
|
||||
def targets(self):
|
||||
"""Return a dictionary of registered targets."""
|
||||
ret = {}
|
||||
for device in self.devices:
|
||||
ret[device.name] = device.name
|
||||
return ret
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_send_message(self, message="", **kwargs):
|
||||
"""Send a notification to knx bus."""
|
||||
if "target" in kwargs:
|
||||
yield from self._async_send_to_device(message, kwargs["target"])
|
||||
else:
|
||||
yield from self._async_send_to_all_devices(message)
|
||||
|
||||
@asyncio.coroutine
|
||||
def _async_send_to_all_devices(self, message):
|
||||
"""Send a notification to knx bus to all connected devices."""
|
||||
for device in self.devices:
|
||||
yield from device.set(message)
|
||||
|
||||
@asyncio.coroutine
|
||||
def _async_send_to_device(self, message, names):
|
||||
"""Send a notification to knx bus to device with given names."""
|
||||
for device in self.devices:
|
||||
if device.name in names:
|
||||
yield from device.set(message)
|
40
homeassistant/components/notify/mycroft.py
Normal file
40
homeassistant/components/notify/mycroft.py
Normal file
|
@ -0,0 +1,40 @@
|
|||
"""
|
||||
Mycroft AI notification platform.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/notify.mycroft/
|
||||
"""
|
||||
import logging
|
||||
|
||||
|
||||
from homeassistant.components.notify import BaseNotificationService
|
||||
|
||||
DEPENDENCIES = ['mycroft']
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_service(hass, config, discovery_info=None):
|
||||
"""Get the Mycroft notification service."""
|
||||
return MycroftNotificationService(
|
||||
hass.data['mycroft'])
|
||||
|
||||
|
||||
class MycroftNotificationService(BaseNotificationService):
|
||||
"""The Mycroft Notification Service."""
|
||||
|
||||
def __init__(self, mycroft_ip):
|
||||
"""Initialize the service."""
|
||||
self.mycroft_ip = mycroft_ip
|
||||
|
||||
def send_message(self, message="", **kwargs):
|
||||
"""Send a message mycroft to speak on instance."""
|
||||
from mycroftapi import MycroftAPI
|
||||
|
||||
text = message
|
||||
mycroft = MycroftAPI(self.mycroft_ip)
|
||||
if mycroft is not None:
|
||||
mycroft.speak_text(text)
|
||||
else:
|
||||
_LOGGER.log("Could not reach this instance of mycroft")
|
|
@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at
|
|||
https://home-assistant.io/components/notify.pushbullet/
|
||||
"""
|
||||
import logging
|
||||
import mimetypes
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
|
@ -20,6 +21,7 @@ _LOGGER = logging.getLogger(__name__)
|
|||
|
||||
ATTR_URL = 'url'
|
||||
ATTR_FILE = 'file'
|
||||
ATTR_FILE_URL = 'file_url'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_API_KEY): cv.string,
|
||||
|
@ -80,16 +82,11 @@ class PushBulletNotificationService(BaseNotificationService):
|
|||
targets = kwargs.get(ATTR_TARGET)
|
||||
title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)
|
||||
data = kwargs.get(ATTR_DATA)
|
||||
url = None
|
||||
filepath = None
|
||||
if data:
|
||||
url = data.get(ATTR_URL, None)
|
||||
filepath = data.get(ATTR_FILE, None)
|
||||
refreshed = False
|
||||
|
||||
if not targets:
|
||||
# Backward compatibility, notify all devices in own account
|
||||
self._push_data(filepath, message, title, self.pushbullet, url)
|
||||
self._push_data(message, title, data, self.pushbullet)
|
||||
_LOGGER.info("Sent notification to self")
|
||||
return
|
||||
|
||||
|
@ -104,8 +101,7 @@ class PushBulletNotificationService(BaseNotificationService):
|
|||
# Target is email, send directly, don't use a target object
|
||||
# This also seems works to send to all devices in own account
|
||||
if ttype == 'email':
|
||||
self._push_data(filepath, message, title, url,
|
||||
self.pushbullet, tname)
|
||||
self._push_data(message, title, data, self.pushbullet, tname)
|
||||
_LOGGER.info("Sent notification to email %s", tname)
|
||||
continue
|
||||
|
||||
|
@ -124,33 +120,47 @@ class PushBulletNotificationService(BaseNotificationService):
|
|||
# Attempt push_note on a dict value. Keys are types & target
|
||||
# name. Dict pbtargets has all *actual* targets.
|
||||
try:
|
||||
self._push_data(filepath, message, title, url,
|
||||
self._push_data(message, title, data,
|
||||
self.pbtargets[ttype][tname])
|
||||
_LOGGER.info("Sent notification to %s/%s", ttype, tname)
|
||||
except KeyError:
|
||||
_LOGGER.error("No such target: %s/%s", ttype, tname)
|
||||
continue
|
||||
|
||||
def _push_data(self, filepath, message, title, url, pusher, tname=None):
|
||||
def _push_data(self, message, title, data, pusher, tname=None):
|
||||
from pushbullet import PushError
|
||||
from pushbullet import Device
|
||||
if data is None:
|
||||
data = {}
|
||||
url = data.get(ATTR_URL)
|
||||
filepath = data.get(ATTR_FILE)
|
||||
file_url = data.get(ATTR_FILE_URL)
|
||||
try:
|
||||
if url:
|
||||
if isinstance(pusher, Device):
|
||||
pusher.push_link(title, url, body=message)
|
||||
else:
|
||||
if tname:
|
||||
pusher.push_link(title, url, body=message, email=tname)
|
||||
elif filepath and self.hass.config.is_allowed_path(filepath):
|
||||
else:
|
||||
pusher.push_link(title, url, body=message)
|
||||
elif filepath:
|
||||
if not self.hass.config.is_allowed_path(filepath):
|
||||
_LOGGER.error("Filepath is not valid or allowed.")
|
||||
return
|
||||
with open(filepath, "rb") as fileh:
|
||||
filedata = self.pushbullet.upload_file(fileh, filepath)
|
||||
if filedata.get('file_type') == 'application/x-empty':
|
||||
_LOGGER.error("Failed to send an empty file.")
|
||||
_LOGGER.error("Can not send an empty file.")
|
||||
return
|
||||
pusher.push_file(title=title, body=message, **filedata)
|
||||
elif file_url:
|
||||
if not file_url.startswith('http'):
|
||||
_LOGGER.error("Url should start with http or https.")
|
||||
return
|
||||
pusher.push_file(title=title, body=message, file_name=file_url,
|
||||
file_url=file_url,
|
||||
file_type=mimetypes.guess_type(file_url)[0])
|
||||
else:
|
||||
if isinstance(pusher, Device):
|
||||
pusher.push_note(title, message)
|
||||
else:
|
||||
if tname:
|
||||
pusher.push_note(title, message, email=tname)
|
||||
else:
|
||||
pusher.push_note(title, message)
|
||||
except PushError as err:
|
||||
_LOGGER.error("Notify failed: %s", err)
|
||||
|
|
|
@ -13,7 +13,7 @@ from homeassistant.components.notify import (
|
|||
from homeassistant.const import (CONF_API_KEY, CONF_SENDER, CONF_RECIPIENT)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
REQUIREMENTS = ['sendgrid==5.0.0']
|
||||
REQUIREMENTS = ['sendgrid==5.2.0']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
@ -8,6 +8,8 @@ import json
|
|||
import logging
|
||||
import mimetypes
|
||||
import os
|
||||
from datetime import timedelta, datetime
|
||||
from functools import partial
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
|
@ -15,6 +17,7 @@ import homeassistant.helpers.config_validation as cv
|
|||
from homeassistant.components.notify import (
|
||||
ATTR_DATA, PLATFORM_SCHEMA, BaseNotificationService)
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_USERNAME
|
||||
from homeassistant.helpers.event import async_track_point_in_time
|
||||
|
||||
REQUIREMENTS = ['TwitterAPI==2.4.6']
|
||||
|
||||
|
@ -68,49 +71,67 @@ class TwitterNotificationService(BaseNotificationService):
|
|||
_LOGGER.warning("'%s' is not a whitelisted directory", media)
|
||||
return
|
||||
|
||||
media_id = self.upload_media(media)
|
||||
callback = partial(self.send_message_callback, message)
|
||||
|
||||
self.upload_media_then_callback(callback, media)
|
||||
|
||||
def send_message_callback(self, message, media_id):
|
||||
"""Tweet a message, optionally with media."""
|
||||
if self.user:
|
||||
resp = self.api.request('direct_messages/new',
|
||||
{'text': message, 'user': self.user,
|
||||
{'user': self.user,
|
||||
'text': message,
|
||||
'media_ids': media_id})
|
||||
else:
|
||||
resp = self.api.request('statuses/update',
|
||||
{'status': message, 'media_ids': media_id})
|
||||
{'status': message,
|
||||
'media_ids': media_id})
|
||||
|
||||
if resp.status_code != 200:
|
||||
self.log_error_resp(resp)
|
||||
else:
|
||||
_LOGGER.debug("Message posted: %s", resp.json())
|
||||
|
||||
def upload_media(self, media_path=None):
|
||||
def upload_media_then_callback(self, callback, media_path=None):
|
||||
"""Upload media."""
|
||||
if not media_path:
|
||||
return None
|
||||
|
||||
with open(media_path, 'rb') as file:
|
||||
total_bytes = os.path.getsize(media_path)
|
||||
(media_category, media_type) = self.media_info(media_path)
|
||||
resp = self.upload_media_init(
|
||||
media_type, media_category, total_bytes
|
||||
)
|
||||
|
||||
if 199 > resp.status_code < 300:
|
||||
self.log_error_resp(resp)
|
||||
return None
|
||||
|
||||
media_id = resp.json()['media_id']
|
||||
media_id = self.upload_media_chunked(file, total_bytes, media_id)
|
||||
|
||||
resp = self.upload_media_finalize(media_id)
|
||||
if 199 > resp.status_code < 300:
|
||||
self.log_error_resp(resp)
|
||||
return None
|
||||
|
||||
self.check_status_until_done(media_id, callback)
|
||||
|
||||
def media_info(self, media_path):
|
||||
"""Determine mime type and Twitter media category for given media."""
|
||||
(media_type, _) = mimetypes.guess_type(media_path)
|
||||
total_bytes = os.path.getsize(media_path)
|
||||
media_category = self.media_category_for_type(media_type)
|
||||
_LOGGER.debug("media %s is mime type %s and translates to %s",
|
||||
media_path, media_type, media_category)
|
||||
return media_category, media_type
|
||||
|
||||
file = open(media_path, 'rb')
|
||||
resp = self.upload_media_init(media_type, total_bytes)
|
||||
|
||||
if 199 > resp.status_code < 300:
|
||||
self.log_error_resp(resp)
|
||||
return None
|
||||
|
||||
media_id = resp.json()['media_id']
|
||||
media_id = self.upload_media_chunked(file, total_bytes, media_id)
|
||||
|
||||
resp = self.upload_media_finalize(media_id)
|
||||
if 199 > resp.status_code < 300:
|
||||
self.log_error_resp(resp)
|
||||
|
||||
return media_id
|
||||
|
||||
def upload_media_init(self, media_type, total_bytes):
|
||||
def upload_media_init(self, media_type, media_category, total_bytes):
|
||||
"""Upload media, INIT phase."""
|
||||
resp = self.api.request('media/upload',
|
||||
return self.api.request('media/upload',
|
||||
{'command': 'INIT', 'media_type': media_type,
|
||||
'media_category': media_category,
|
||||
'total_bytes': total_bytes})
|
||||
return resp
|
||||
|
||||
def upload_media_chunked(self, file, total_bytes, media_id):
|
||||
"""Upload media, chunked append."""
|
||||
|
@ -128,17 +149,55 @@ class TwitterNotificationService(BaseNotificationService):
|
|||
return media_id
|
||||
|
||||
def upload_media_append(self, chunk, media_id, segment_id):
|
||||
"""Upload media, append phase."""
|
||||
"""Upload media, APPEND phase."""
|
||||
return self.api.request('media/upload',
|
||||
{'command': 'APPEND', 'media_id': media_id,
|
||||
'segment_index': segment_id},
|
||||
{'media': chunk})
|
||||
|
||||
def upload_media_finalize(self, media_id):
|
||||
"""Upload media, finalize phase."""
|
||||
"""Upload media, FINALIZE phase."""
|
||||
return self.api.request('media/upload',
|
||||
{'command': 'FINALIZE', 'media_id': media_id})
|
||||
|
||||
def check_status_until_done(self, media_id, callback, *args):
|
||||
"""Upload media, STATUS phase."""
|
||||
resp = self.api.request('media/upload',
|
||||
{'command': 'STATUS', 'media_id': media_id},
|
||||
method_override='GET')
|
||||
if resp.status_code != 200:
|
||||
_LOGGER.error("media processing error: %s", resp.json())
|
||||
processing_info = resp.json()['processing_info']
|
||||
|
||||
_LOGGER.debug("media processing %s status: %s", media_id,
|
||||
processing_info)
|
||||
|
||||
if processing_info['state'] in {u'succeeded', u'failed'}:
|
||||
return callback(media_id)
|
||||
|
||||
check_after_secs = processing_info['check_after_secs']
|
||||
_LOGGER.debug("media processing waiting %s seconds to check status",
|
||||
str(check_after_secs))
|
||||
|
||||
when = datetime.now() + timedelta(seconds=check_after_secs)
|
||||
myself = partial(self.check_status_until_done, media_id, callback)
|
||||
async_track_point_in_time(self.hass, myself, when)
|
||||
|
||||
@staticmethod
|
||||
def media_category_for_type(media_type):
|
||||
"""Determine Twitter media category by mime type."""
|
||||
if media_type is None:
|
||||
return None
|
||||
|
||||
if media_type.startswith('image/gif'):
|
||||
return 'tweet_gif'
|
||||
elif media_type.startswith('video/'):
|
||||
return 'tweet_video'
|
||||
elif media_type.startswith('image/'):
|
||||
return 'tweet_image'
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def log_bytes_sent(bytes_sent, total_bytes):
|
||||
"""Log upload progress."""
|
||||
|
|
|
@ -15,18 +15,20 @@ from homeassistant.const import CONF_PASSWORD, CONF_SENDER, CONF_RECIPIENT
|
|||
|
||||
REQUIREMENTS = ['sleekxmpp==1.3.2',
|
||||
'dnspython3==1.15.0',
|
||||
'pyasn1==0.3.2',
|
||||
'pyasn1-modules==0.0.11']
|
||||
'pyasn1==0.3.3',
|
||||
'pyasn1-modules==0.1.1']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_TLS = 'tls'
|
||||
CONF_VERIFY = 'verify'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_SENDER): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_RECIPIENT): cv.string,
|
||||
vol.Optional(CONF_TLS, default=True): cv.boolean,
|
||||
vol.Optional(CONF_VERIFY, default=True): cv.boolean,
|
||||
})
|
||||
|
||||
|
||||
|
@ -34,18 +36,20 @@ def get_service(hass, config, discovery_info=None):
|
|||
"""Get the Jabber (XMPP) notification service."""
|
||||
return XmppNotificationService(
|
||||
config.get(CONF_SENDER), config.get(CONF_PASSWORD),
|
||||
config.get(CONF_RECIPIENT), config.get(CONF_TLS))
|
||||
config.get(CONF_RECIPIENT), config.get(CONF_TLS),
|
||||
config.get(CONF_VERIFY))
|
||||
|
||||
|
||||
class XmppNotificationService(BaseNotificationService):
|
||||
"""Implement the notification service for Jabber (XMPP)."""
|
||||
|
||||
def __init__(self, sender, password, recipient, tls):
|
||||
def __init__(self, sender, password, recipient, tls, verify):
|
||||
"""Initialize the service."""
|
||||
self._sender = sender
|
||||
self._password = password
|
||||
self._recipient = recipient
|
||||
self._tls = tls
|
||||
self._verify = verify
|
||||
|
||||
def send_message(self, message="", **kwargs):
|
||||
"""Send a message to a user."""
|
||||
|
@ -53,10 +57,11 @@ class XmppNotificationService(BaseNotificationService):
|
|||
data = '{}: {}'.format(title, message) if title else message
|
||||
|
||||
send_message('{}/home-assistant'.format(self._sender), self._password,
|
||||
self._recipient, self._tls, data)
|
||||
self._recipient, self._tls, self._verify, data)
|
||||
|
||||
|
||||
def send_message(sender, password, recipient, use_tls, message):
|
||||
def send_message(sender, password, recipient, use_tls,
|
||||
verify_certificate, message):
|
||||
"""Send a message over XMPP."""
|
||||
import sleekxmpp
|
||||
|
||||
|
@ -73,6 +78,10 @@ def send_message(sender, password, recipient, use_tls, message):
|
|||
self.use_ipv6 = False
|
||||
self.add_event_handler('failed_auth', self.check_credentials)
|
||||
self.add_event_handler('session_start', self.start)
|
||||
if not verify_certificate:
|
||||
self.add_event_handler('ssl_invalid_cert',
|
||||
self.discard_ssl_invalid_cert)
|
||||
|
||||
self.connect(use_tls=self.use_tls, use_ssl=False)
|
||||
self.process()
|
||||
|
||||
|
@ -87,4 +96,10 @@ def send_message(sender, password, recipient, use_tls, message):
|
|||
"""Disconnect from the server if credentials are invalid."""
|
||||
self.disconnect()
|
||||
|
||||
@staticmethod
|
||||
def discard_ssl_invalid_cert(event):
|
||||
"""Do nothing if ssl certificate is invalid."""
|
||||
_LOGGER.info('Ignoring invalid ssl certificate as requested.')
|
||||
return
|
||||
|
||||
SendNotificationBot()
|
||||
|
|
|
@ -4,6 +4,7 @@ Support for RFXtrx components.
|
|||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/rfxtrx/
|
||||
"""
|
||||
|
||||
import logging
|
||||
from collections import OrderedDict
|
||||
import voluptuous as vol
|
||||
|
@ -11,13 +12,14 @@ import voluptuous as vol
|
|||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util import slugify
|
||||
from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_START,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
ATTR_ENTITY_ID, TEMP_CELSIUS,
|
||||
CONF_DEVICE_CLASS, CONF_COMMAND_ON, CONF_COMMAND_OFF
|
||||
)
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
REQUIREMENTS = ['pyRFXtrx==0.19.0']
|
||||
REQUIREMENTS = ['pyRFXtrx==0.20.1']
|
||||
|
||||
DOMAIN = 'rfxtrx'
|
||||
|
||||
|
@ -54,7 +56,7 @@ DATA_TYPES = OrderedDict([
|
|||
RECEIVED_EVT_SUBSCRIBERS = []
|
||||
RFX_DEVICES = {}
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
RFXOBJECT = None
|
||||
RFXOBJECT = 'rfxobject'
|
||||
|
||||
|
||||
def _valid_device(value, device_type):
|
||||
|
@ -77,10 +79,6 @@ def _valid_device(value, device_type):
|
|||
if not len(key) % 2 == 0:
|
||||
key = '0' + key
|
||||
|
||||
if get_rfx_object(key) is None:
|
||||
raise vol.Invalid('Rfxtrx device {} is invalid: '
|
||||
'Invalid device id for {}'.format(key, value))
|
||||
|
||||
if device_type == 'sensor':
|
||||
config[key] = DEVICE_SCHEMA_SENSOR(device)
|
||||
elif device_type == 'binary_sensor':
|
||||
|
@ -171,24 +169,24 @@ def setup(hass, config):
|
|||
# Try to load the RFXtrx module.
|
||||
import RFXtrx as rfxtrxmod
|
||||
|
||||
# Init the rfxtrx module.
|
||||
global RFXOBJECT
|
||||
|
||||
device = config[DOMAIN][ATTR_DEVICE]
|
||||
debug = config[DOMAIN][ATTR_DEBUG]
|
||||
dummy_connection = config[DOMAIN][ATTR_DUMMY]
|
||||
|
||||
if dummy_connection:
|
||||
RFXOBJECT =\
|
||||
rfxtrxmod.Connect(device, handle_receive, debug=debug,
|
||||
hass.data[RFXOBJECT] =\
|
||||
rfxtrxmod.Connect(device, None, debug=debug,
|
||||
transport_protocol=rfxtrxmod.DummyTransport2)
|
||||
else:
|
||||
RFXOBJECT = rfxtrxmod.Connect(device, handle_receive, debug=debug)
|
||||
hass.data[RFXOBJECT] = rfxtrxmod.Connect(device, None, debug=debug)
|
||||
|
||||
def _start_rfxtrx(event):
|
||||
hass.data[RFXOBJECT].event_callback = handle_receive
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, _start_rfxtrx)
|
||||
|
||||
def _shutdown_rfxtrx(event):
|
||||
"""Close connection with RFXtrx."""
|
||||
RFXOBJECT.close_connection()
|
||||
|
||||
hass.data[RFXOBJECT].close_connection()
|
||||
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, _shutdown_rfxtrx)
|
||||
|
||||
return True
|
||||
|
@ -285,13 +283,16 @@ def find_possible_pt2262_device(device_id):
|
|||
return None
|
||||
|
||||
|
||||
def get_devices_from_config(config, device, hass):
|
||||
def get_devices_from_config(config, device):
|
||||
"""Read rfxtrx configuration."""
|
||||
signal_repetitions = config[CONF_SIGNAL_REPETITIONS]
|
||||
|
||||
devices = []
|
||||
for packet_id, entity_info in config[CONF_DEVICES].items():
|
||||
event = get_rfx_object(packet_id)
|
||||
if event is None:
|
||||
_LOGGER.error("Invalid device: %s", packet_id)
|
||||
continue
|
||||
device_id = slugify(event.device.id_string.lower())
|
||||
if device_id in RFX_DEVICES:
|
||||
continue
|
||||
|
@ -303,13 +304,12 @@ def get_devices_from_config(config, device, hass):
|
|||
|
||||
new_device = device(entity_info[ATTR_NAME], event, datas,
|
||||
signal_repetitions)
|
||||
new_device.hass = hass
|
||||
RFX_DEVICES[device_id] = new_device
|
||||
devices.append(new_device)
|
||||
return devices
|
||||
|
||||
|
||||
def get_new_device(event, config, device, hass):
|
||||
def get_new_device(event, config, device):
|
||||
"""Add entity if not exist and the automatic_add is True."""
|
||||
device_id = slugify(event.device.id_string.lower())
|
||||
if device_id in RFX_DEVICES:
|
||||
|
@ -330,7 +330,6 @@ def get_new_device(event, config, device, hass):
|
|||
signal_repetitions = config[CONF_SIGNAL_REPETITIONS]
|
||||
new_device = device(pkt_id, event, datas,
|
||||
signal_repetitions)
|
||||
new_device.hass = hass
|
||||
RFX_DEVICES[device_id] = new_device
|
||||
return new_device
|
||||
|
||||
|
@ -438,31 +437,36 @@ class RfxtrxDevice(Entity):
|
|||
|
||||
if command == "turn_on":
|
||||
for _ in range(self.signal_repetitions):
|
||||
self._event.device.send_on(RFXOBJECT.transport)
|
||||
self._event.device.send_on(self.hass.data[RFXOBJECT]
|
||||
.transport)
|
||||
self._state = True
|
||||
|
||||
elif command == "dim":
|
||||
for _ in range(self.signal_repetitions):
|
||||
self._event.device.send_dim(RFXOBJECT.transport,
|
||||
brightness)
|
||||
self._event.device.send_dim(self.hass.data[RFXOBJECT]
|
||||
.transport, brightness)
|
||||
self._state = True
|
||||
|
||||
elif command == 'turn_off':
|
||||
for _ in range(self.signal_repetitions):
|
||||
self._event.device.send_off(RFXOBJECT.transport)
|
||||
self._event.device.send_off(self.hass.data[RFXOBJECT]
|
||||
.transport)
|
||||
self._state = False
|
||||
self._brightness = 0
|
||||
|
||||
elif command == "roll_up":
|
||||
for _ in range(self.signal_repetitions):
|
||||
self._event.device.send_open(RFXOBJECT.transport)
|
||||
self._event.device.send_open(self.hass.data[RFXOBJECT]
|
||||
.transport)
|
||||
|
||||
elif command == "roll_down":
|
||||
for _ in range(self.signal_repetitions):
|
||||
self._event.device.send_close(RFXOBJECT.transport)
|
||||
self._event.device.send_close(self.hass.data[RFXOBJECT]
|
||||
.transport)
|
||||
|
||||
elif command == "stop_roll":
|
||||
for _ in range(self.signal_repetitions):
|
||||
self._event.device.send_stop(RFXOBJECT.transport)
|
||||
self._event.device.send_stop(self.hass.data[RFXOBJECT]
|
||||
.transport)
|
||||
|
||||
self.schedule_update_ha_state()
|
||||
|
|
289
homeassistant/components/sensor/airvisual.py
Normal file
289
homeassistant/components/sensor/airvisual.py
Normal file
|
@ -0,0 +1,289 @@
|
|||
"""
|
||||
Support for AirVisual air quality sensors.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.airvisual/
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from logging import getLogger
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||
from homeassistant.const import (ATTR_ATTRIBUTION, ATTR_STATE, CONF_API_KEY,
|
||||
CONF_LATITUDE, CONF_LONGITUDE,
|
||||
CONF_MONITORED_CONDITIONS)
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
_LOGGER = getLogger(__name__)
|
||||
REQUIREMENTS = ['pyairvisual==0.1.0']
|
||||
|
||||
ATTR_CITY = 'city'
|
||||
ATTR_COUNTRY = 'country'
|
||||
ATTR_POLLUTANT_SYMBOL = 'pollutant_symbol'
|
||||
ATTR_POLLUTANT_UNIT = 'pollutant_unit'
|
||||
ATTR_TIMESTAMP = 'timestamp'
|
||||
|
||||
CONF_RADIUS = 'radius'
|
||||
|
||||
MASS_PARTS_PER_MILLION = 'ppm'
|
||||
MASS_PARTS_PER_BILLION = 'ppb'
|
||||
VOLUME_MICROGRAMS_PER_CUBIC_METER = 'µg/m3'
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10)
|
||||
|
||||
POLLUTANT_LEVEL_MAPPING = [{
|
||||
'label': 'Good',
|
||||
'minimum': 0,
|
||||
'maximum': 50
|
||||
}, {
|
||||
'label': 'Moderate',
|
||||
'minimum': 51,
|
||||
'maximum': 100
|
||||
}, {
|
||||
'label': 'Unhealthy for Sensitive Groups',
|
||||
'minimum': 101,
|
||||
'maximum': 150
|
||||
}, {
|
||||
'label': 'Unhealthy',
|
||||
'minimum': 151,
|
||||
'maximum': 200
|
||||
}, {
|
||||
'label': 'Very Unhealthy',
|
||||
'minimum': 201,
|
||||
'maximum': 300
|
||||
}, {
|
||||
'label': 'Hazardous',
|
||||
'minimum': 301,
|
||||
'maximum': 10000
|
||||
}]
|
||||
POLLUTANT_MAPPING = {
|
||||
'co': {
|
||||
'label': 'Carbon Monoxide',
|
||||
'unit': MASS_PARTS_PER_MILLION
|
||||
},
|
||||
'n2': {
|
||||
'label': 'Nitrogen Dioxide',
|
||||
'unit': MASS_PARTS_PER_BILLION
|
||||
},
|
||||
'o3': {
|
||||
'label': 'Ozone',
|
||||
'unit': MASS_PARTS_PER_BILLION
|
||||
},
|
||||
'p1': {
|
||||
'label': 'PM10',
|
||||
'unit': VOLUME_MICROGRAMS_PER_CUBIC_METER
|
||||
},
|
||||
'p2': {
|
||||
'label': 'PM2.5',
|
||||
'unit': VOLUME_MICROGRAMS_PER_CUBIC_METER
|
||||
},
|
||||
's2': {
|
||||
'label': 'Sulfur Dioxide',
|
||||
'unit': MASS_PARTS_PER_BILLION
|
||||
}
|
||||
}
|
||||
|
||||
SENSOR_LOCALES = {'cn': 'Chinese', 'us': 'U.S.'}
|
||||
SENSOR_TYPES = [
|
||||
('AirPollutionLevelSensor', 'Air Pollution Level', 'mdi:scale'),
|
||||
('AirQualityIndexSensor', 'Air Quality Index', 'mdi:format-list-numbers'),
|
||||
('MainPollutantSensor', 'Main Pollutant', 'mdi:chemical-weapon'),
|
||||
]
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_API_KEY):
|
||||
cv.string,
|
||||
vol.Required(CONF_MONITORED_CONDITIONS):
|
||||
vol.All(cv.ensure_list, [vol.In(SENSOR_LOCALES)]),
|
||||
vol.Optional(CONF_LATITUDE):
|
||||
cv.latitude,
|
||||
vol.Optional(CONF_LONGITUDE):
|
||||
cv.longitude,
|
||||
vol.Optional(CONF_RADIUS, default=1000):
|
||||
cv.positive_int,
|
||||
})
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Configure the platform and add the sensors."""
|
||||
import pyairvisual as pav
|
||||
|
||||
api_key = config.get(CONF_API_KEY)
|
||||
_LOGGER.debug('AirVisual API Key: %s', api_key)
|
||||
|
||||
monitored_locales = config.get(CONF_MONITORED_CONDITIONS)
|
||||
_LOGGER.debug('Monitored Conditions: %s', monitored_locales)
|
||||
|
||||
latitude = config.get(CONF_LATITUDE, hass.config.latitude)
|
||||
_LOGGER.debug('AirVisual Latitude: %s', latitude)
|
||||
|
||||
longitude = config.get(CONF_LONGITUDE, hass.config.longitude)
|
||||
_LOGGER.debug('AirVisual Longitude: %s', longitude)
|
||||
|
||||
radius = config.get(CONF_RADIUS)
|
||||
_LOGGER.debug('AirVisual Radius: %s', radius)
|
||||
|
||||
data = AirVisualData(pav.Client(api_key), latitude, longitude, radius)
|
||||
|
||||
sensors = []
|
||||
for locale in monitored_locales:
|
||||
for sensor_class, name, icon in SENSOR_TYPES:
|
||||
sensors.append(globals()[sensor_class](data, name, icon, locale))
|
||||
|
||||
async_add_devices(sensors, True)
|
||||
|
||||
|
||||
def merge_two_dicts(dict1, dict2):
|
||||
"""Merge two dicts into a new dict as a shallow copy."""
|
||||
final = dict1.copy()
|
||||
final.update(dict2)
|
||||
return final
|
||||
|
||||
|
||||
class AirVisualBaseSensor(Entity):
|
||||
"""Define a base class for all of our sensors."""
|
||||
|
||||
def __init__(self, data, name, icon, locale):
|
||||
"""Initialize."""
|
||||
self._data = data
|
||||
self._icon = icon
|
||||
self._locale = locale
|
||||
self._name = name
|
||||
self._state = None
|
||||
self._unit = None
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
if self._data:
|
||||
return {
|
||||
ATTR_ATTRIBUTION: 'AirVisual©',
|
||||
ATTR_CITY: self._data.city,
|
||||
ATTR_COUNTRY: self._data.country,
|
||||
ATTR_STATE: self._data.state,
|
||||
ATTR_TIMESTAMP: self._data.pollution_info.get('ts')
|
||||
}
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon."""
|
||||
return self._icon
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name."""
|
||||
return '{0} {1}'.format(SENSOR_LOCALES[self._locale], self._name)
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state."""
|
||||
return self._state
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
"""Update the status of the sensor."""
|
||||
_LOGGER.debug('updating sensor: %s', self._name)
|
||||
self._data.update()
|
||||
|
||||
|
||||
class AirPollutionLevelSensor(AirVisualBaseSensor):
|
||||
"""Define a sensor to measure air pollution level."""
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
"""Update the status of the sensor."""
|
||||
yield from super().async_update()
|
||||
aqi = self._data.pollution_info.get('aqi{0}'.format(self._locale))
|
||||
|
||||
try:
|
||||
[level] = [
|
||||
i for i in POLLUTANT_LEVEL_MAPPING
|
||||
if i['minimum'] <= aqi <= i['maximum']
|
||||
]
|
||||
self._state = level.get('label')
|
||||
except ValueError:
|
||||
self._state = None
|
||||
|
||||
|
||||
class AirQualityIndexSensor(AirVisualBaseSensor):
|
||||
"""Define a sensor to measure AQI."""
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit the value is expressed in."""
|
||||
return ''
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
"""Update the status of the sensor."""
|
||||
yield from super().async_update()
|
||||
self._state = self._data.pollution_info.get(
|
||||
'aqi{0}'.format(self._locale))
|
||||
|
||||
|
||||
class MainPollutantSensor(AirVisualBaseSensor):
|
||||
"""Define a sensor to the main pollutant of an area."""
|
||||
|
||||
def __init__(self, data, name, icon, locale):
|
||||
"""Initialize."""
|
||||
super().__init__(data, name, icon, locale)
|
||||
self._symbol = None
|
||||
self._unit = None
|
||||
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
if self._data:
|
||||
return merge_two_dicts(super().device_state_attributes, {
|
||||
ATTR_POLLUTANT_SYMBOL: self._symbol,
|
||||
ATTR_POLLUTANT_UNIT: self._unit
|
||||
})
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_update(self):
|
||||
"""Update the status of the sensor."""
|
||||
yield from super().async_update()
|
||||
symbol = self._data.pollution_info.get('main{0}'.format(self._locale))
|
||||
pollution_info = POLLUTANT_MAPPING.get(symbol, {})
|
||||
self._state = pollution_info.get('label')
|
||||
self._unit = pollution_info.get('unit')
|
||||
self._symbol = symbol
|
||||
|
||||
|
||||
class AirVisualData(object):
|
||||
"""Define an object to hold sensor data."""
|
||||
|
||||
def __init__(self, client, latitude, longitude, radius):
|
||||
"""Initialize."""
|
||||
self.city = None
|
||||
self._client = client
|
||||
self.country = None
|
||||
self.latitude = latitude
|
||||
self.longitude = longitude
|
||||
self.pollution_info = None
|
||||
self.radius = radius
|
||||
self.state = None
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Update with new AirVisual data."""
|
||||
import pyairvisual.exceptions as exceptions
|
||||
|
||||
try:
|
||||
resp = self._client.nearest_city(self.latitude, self.longitude,
|
||||
self.radius).get('data')
|
||||
_LOGGER.debug('New data retrieved: %s', resp)
|
||||
|
||||
self.city = resp.get('city')
|
||||
self.state = resp.get('state')
|
||||
self.country = resp.get('country')
|
||||
self.pollution_info = resp.get('current').get('pollution')
|
||||
except exceptions.HTTPError as exc_info:
|
||||
_LOGGER.error('Unable to update sensor data')
|
||||
_LOGGER.debug(exc_info)
|
|
@ -220,7 +220,12 @@ class BrSensor(Entity):
|
|||
|
||||
# update all other sensors
|
||||
if self.type.startswith(SYMBOL) or self.type.startswith(CONDITION):
|
||||
condition = data.get(FORECAST)[fcday].get(CONDITION)
|
||||
try:
|
||||
condition = data.get(FORECAST)[fcday].get(CONDITION)
|
||||
except IndexError:
|
||||
_LOGGER.warning("No forecast for fcday=%s...", fcday)
|
||||
return False
|
||||
|
||||
if condition:
|
||||
new_state = condition.get(CONDITION, None)
|
||||
if self.type.startswith(SYMBOL):
|
||||
|
@ -240,7 +245,11 @@ class BrSensor(Entity):
|
|||
return True
|
||||
return False
|
||||
else:
|
||||
new_state = data.get(FORECAST)[fcday].get(self.type[:-3])
|
||||
try:
|
||||
new_state = data.get(FORECAST)[fcday].get(self.type[:-3])
|
||||
except IndexError:
|
||||
_LOGGER.warning("No forecast for fcday=%s...", fcday)
|
||||
return False
|
||||
|
||||
if new_state != self._state:
|
||||
self._state = new_state
|
||||
|
|
|
@ -127,7 +127,7 @@ class DHTSensor(Entity):
|
|||
humidity_offset = self.humidity_offset
|
||||
data = self.dht_client.data
|
||||
|
||||
if self.type == SENSOR_TEMPERATURE:
|
||||
if self.type == SENSOR_TEMPERATURE and SENSOR_TEMPERATURE in data:
|
||||
temperature = data[SENSOR_TEMPERATURE]
|
||||
_LOGGER.debug("Temperature %.1f \u00b0C + offset %.1f",
|
||||
temperature, temperature_offset)
|
||||
|
@ -135,7 +135,7 @@ class DHTSensor(Entity):
|
|||
self._state = round(temperature + temperature_offset, 1)
|
||||
if self.temp_unit == TEMP_FAHRENHEIT:
|
||||
self._state = round(celsius_to_fahrenheit(temperature), 1)
|
||||
elif self.type == SENSOR_HUMIDITY:
|
||||
elif self.type == SENSOR_HUMIDITY and SENSOR_HUMIDITY in data:
|
||||
humidity = data[SENSOR_HUMIDITY]
|
||||
_LOGGER.debug("Humidity %.1f%% + offset %.1f",
|
||||
humidity, humidity_offset)
|
||||
|
|
243
homeassistant/components/sensor/dwd_weather_warnings.py
Normal file
243
homeassistant/components/sensor/dwd_weather_warnings.py
Normal file
|
@ -0,0 +1,243 @@
|
|||
"""
|
||||
Support for getting statistical data from a DWD Weather Warnings.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.dwd_weather_warnings/
|
||||
|
||||
Data is fetched from DWD:
|
||||
https://rcccm.dwd.de/DE/wetter/warnungen_aktuell/objekt_einbindung/objekteinbindung.html
|
||||
|
||||
Warnungen vor extremem Unwetter (Stufe 4)
|
||||
Unwetterwarnungen (Stufe 3)
|
||||
Warnungen vor markantem Wetter (Stufe 2)
|
||||
Wetterwarnungen (Stufe 1)
|
||||
"""
|
||||
import logging
|
||||
import json
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||
from homeassistant.const import (
|
||||
ATTR_ATTRIBUTION, CONF_NAME, CONF_MONITORED_CONDITIONS)
|
||||
from homeassistant.util import Throttle
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.components.sensor.rest import RestData
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTRIBUTION = "Data provided by DWD"
|
||||
|
||||
DEFAULT_NAME = 'DWD-Weather-Warnings'
|
||||
|
||||
CONF_REGION_NAME = 'region_name'
|
||||
|
||||
SCAN_INTERVAL = timedelta(minutes=15)
|
||||
|
||||
MONITORED_CONDITIONS = {
|
||||
'current_warning_level': ['Current Warning Level',
|
||||
None, 'mdi:close-octagon-outline'],
|
||||
'advance_warning_level': ['Advance Warning Level',
|
||||
None, 'mdi:close-octagon-outline'],
|
||||
}
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_REGION_NAME): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_MONITORED_CONDITIONS, default=MONITORED_CONDITIONS):
|
||||
vol.All(cv.ensure_list, [vol.In(MONITORED_CONDITIONS)]),
|
||||
})
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
"""Set up the DWD-Weather-Warnings sensor."""
|
||||
name = config.get(CONF_NAME)
|
||||
region_name = config.get(CONF_REGION_NAME)
|
||||
|
||||
api = DwdWeatherWarningsAPI(region_name)
|
||||
|
||||
sensors = [DwdWeatherWarningsSensor(api, name, condition)
|
||||
for condition in config[CONF_MONITORED_CONDITIONS]]
|
||||
|
||||
add_devices(sensors, True)
|
||||
|
||||
|
||||
class DwdWeatherWarningsSensor(Entity):
|
||||
"""Representation of a DWD-Weather-Warnings sensor."""
|
||||
|
||||
def __init__(self, api, name, variable):
|
||||
"""Initialize a DWD-Weather-Warnings sensor."""
|
||||
self._api = api
|
||||
self._name = name
|
||||
self._var_id = variable
|
||||
|
||||
variable_info = MONITORED_CONDITIONS[variable]
|
||||
self._var_name = variable_info[0]
|
||||
self._var_units = variable_info[1]
|
||||
self._var_icon = variable_info[2]
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return "{} {}".format(self._name, self._var_name)
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Icon to use in the frontend, if any."""
|
||||
return self._var_icon
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit the value is expressed in."""
|
||||
return self._var_units
|
||||
|
||||
# pylint: disable=no-member
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
try:
|
||||
return round(self._api.data[self._var_id], 2)
|
||||
except TypeError:
|
||||
return self._api.data[self._var_id]
|
||||
|
||||
# pylint: disable=no-member
|
||||
@property
|
||||
def device_state_attributes(self):
|
||||
"""Return the state attributes of the DWD-Weather-Warnings."""
|
||||
data = {
|
||||
ATTR_ATTRIBUTION: ATTRIBUTION,
|
||||
'region_name': self._api.region_name
|
||||
}
|
||||
|
||||
if self._api.region_id is not None:
|
||||
data['region_id'] = self._api.region_id
|
||||
|
||||
if self._api.region_state is not None:
|
||||
data['region_state'] = self._api.region_state
|
||||
|
||||
if self._api.data['time'] is not None:
|
||||
data['last_update'] = dt_util.as_local(
|
||||
dt_util.utc_from_timestamp(self._api.data['time'] / 1000))
|
||||
|
||||
if self._var_id == 'current_warning_level':
|
||||
prefix = 'current'
|
||||
elif self._var_id == 'advance_warning_level':
|
||||
prefix = 'advance'
|
||||
else:
|
||||
raise Exception('Unknown warning type')
|
||||
|
||||
data['warning_count'] = self._api.data[prefix + '_warning_count']
|
||||
i = 0
|
||||
for event in self._api.data[prefix + '_warnings']:
|
||||
i = i + 1
|
||||
|
||||
data['warning_{}_name'.format(i)] = event['event']
|
||||
data['warning_{}_level'.format(i)] = event['level']
|
||||
data['warning_{}_type'.format(i)] = event['type']
|
||||
if len(event['headline']) > 0:
|
||||
data['warning_{}_headline'.format(i)] = event['headline']
|
||||
if len(event['description']) > 0:
|
||||
data['warning_{}_description'.format(i)] = event['description']
|
||||
if len(event['instruction']) > 0:
|
||||
data['warning_{}_instruction'.format(i)] = event['instruction']
|
||||
|
||||
if event['start'] is not None:
|
||||
data['warning_{}_start'.format(i)] = dt_util.as_local(
|
||||
dt_util.utc_from_timestamp(event['start'] / 1000))
|
||||
|
||||
if event['end'] is not None:
|
||||
data['warning_{}_end'.format(i)] = dt_util.as_local(
|
||||
dt_util.utc_from_timestamp(event['end'] / 1000))
|
||||
|
||||
return data
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Could the device be accessed during the last update call."""
|
||||
return self._api.available
|
||||
|
||||
def update(self):
|
||||
"""Get the latest data from the DWD-Weather-Warnings API."""
|
||||
self._api.update()
|
||||
|
||||
|
||||
class DwdWeatherWarningsAPI(object):
|
||||
"""Get the latest data and update the states."""
|
||||
|
||||
def __init__(self, region_name):
|
||||
"""Initialize the data object."""
|
||||
resource = "{}{}{}?{}".format(
|
||||
'https://',
|
||||
'www.dwd.de',
|
||||
'/DWD/warnungen/warnapp_landkreise/json/warnings.json',
|
||||
'jsonp=loadWarnings'
|
||||
)
|
||||
|
||||
self._rest = RestData('GET', resource, None, None, None, True)
|
||||
self.region_name = region_name
|
||||
self.region_id = None
|
||||
self.region_state = None
|
||||
self.data = None
|
||||
self.available = True
|
||||
self.update()
|
||||
|
||||
@Throttle(SCAN_INTERVAL)
|
||||
def update(self):
|
||||
"""Get the latest data from the DWD-Weather-Warnings."""
|
||||
try:
|
||||
self._rest.update()
|
||||
|
||||
json_string = self._rest.data[24:len(self._rest.data) - 2]
|
||||
json_obj = json.loads(json_string)
|
||||
|
||||
data = {'time': json_obj['time']}
|
||||
|
||||
for mykey, myvalue in {
|
||||
'current': 'warnings',
|
||||
'advance': 'vorabInformation'
|
||||
}.items():
|
||||
|
||||
_LOGGER.debug("Found %d %s global DWD warnings",
|
||||
len(json_obj[myvalue]), mykey)
|
||||
|
||||
data['{}_warning_level'.format(mykey)] = 0
|
||||
my_warnings = []
|
||||
|
||||
if self.region_id is not None:
|
||||
# get a specific region_id
|
||||
if self.region_id in json_obj[myvalue]:
|
||||
my_warnings = json_obj[myvalue][self.region_id]
|
||||
|
||||
else:
|
||||
# loop through all items to find warnings, region_id
|
||||
# and region_state for region_name
|
||||
for key in json_obj[myvalue]:
|
||||
my_region = json_obj[myvalue][key][0]['regionName']
|
||||
if my_region != self.region_name:
|
||||
continue
|
||||
my_warnings = json_obj[myvalue][key]
|
||||
my_state = json_obj[myvalue][key][0]['stateShort']
|
||||
self.region_id = key
|
||||
self.region_state = my_state
|
||||
break
|
||||
|
||||
# Get max warning level
|
||||
maxlevel = data['{}_warning_level'.format(mykey)]
|
||||
for event in my_warnings:
|
||||
if event['level'] >= maxlevel:
|
||||
data['{}_warning_level'.format(mykey)] = event['level']
|
||||
|
||||
data['{}_warning_count'.format(mykey)] = len(my_warnings)
|
||||
data['{}_warnings'.format(mykey)] = my_warnings
|
||||
|
||||
_LOGGER.debug("Found %d %s local DWD warnings",
|
||||
len(my_warnings), mykey)
|
||||
|
||||
self.data = data
|
||||
self.available = True
|
||||
except TypeError:
|
||||
_LOGGER.error("Unable to fetch data from DWD-Weather-Warnings")
|
||||
self.available = False
|
|
@ -260,13 +260,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||
|
||||
access_token = config_file.get(ATTR_ACCESS_TOKEN)
|
||||
refresh_token = config_file.get(ATTR_REFRESH_TOKEN)
|
||||
expires_at = config_file.get(ATTR_LAST_SAVED_AT)
|
||||
if None not in (access_token, refresh_token):
|
||||
authd_client = fitbit.Fitbit(config_file.get(ATTR_CLIENT_ID),
|
||||
config_file.get(ATTR_CLIENT_SECRET),
|
||||
access_token=access_token,
|
||||
refresh_token=refresh_token)
|
||||
refresh_token=refresh_token,
|
||||
expires_at=expires_at,
|
||||
refresh_cb=lambda x: None)
|
||||
|
||||
if int(time.time()) - config_file.get(ATTR_LAST_SAVED_AT, 0) > 3600:
|
||||
if int(time.time()) - expires_at > 3600:
|
||||
authd_client.client.refresh_token()
|
||||
|
||||
authd_client.system = authd_client.user_profile_get()["user"]["locale"]
|
||||
|
@ -338,12 +341,14 @@ class FitbitAuthCallbackView(HomeAssistantView):
|
|||
response_message = """Fitbit has been successfully authorized!
|
||||
You can close this window now!"""
|
||||
|
||||
result = None
|
||||
if data.get('code') is not None:
|
||||
redirect_uri = '{}{}'.format(
|
||||
hass.config.api.base_url, FITBIT_AUTH_CALLBACK_PATH)
|
||||
|
||||
try:
|
||||
self.oauth.fetch_access_token(data.get('code'), redirect_uri)
|
||||
result = self.oauth.fetch_access_token(data.get('code'),
|
||||
redirect_uri)
|
||||
except MissingTokenError as error:
|
||||
_LOGGER.error("Missing token: %s", error)
|
||||
response_message = """Something went wrong when
|
||||
|
@ -361,15 +366,23 @@ class FitbitAuthCallbackView(HomeAssistantView):
|
|||
An unknown error occurred. Please try again!
|
||||
"""
|
||||
|
||||
if result is None:
|
||||
_LOGGER.error("Unknown error when authing")
|
||||
response_message = """Something went wrong when
|
||||
attempting authenticating with Fitbit.
|
||||
An unknown error occurred. Please try again!
|
||||
"""
|
||||
|
||||
html_response = """<html><head><title>Fitbit Auth</title></head>
|
||||
<body><h1>{}</h1></body></html>""".format(response_message)
|
||||
|
||||
config_contents = {
|
||||
ATTR_ACCESS_TOKEN: self.oauth.token['access_token'],
|
||||
ATTR_REFRESH_TOKEN: self.oauth.token['refresh_token'],
|
||||
ATTR_CLIENT_ID: self.oauth.client_id,
|
||||
ATTR_CLIENT_SECRET: self.oauth.client_secret
|
||||
}
|
||||
if result:
|
||||
config_contents = {
|
||||
ATTR_ACCESS_TOKEN: result.get('access_token'),
|
||||
ATTR_REFRESH_TOKEN: result.get('refresh_token'),
|
||||
ATTR_CLIENT_ID: self.oauth.client_id,
|
||||
ATTR_CLIENT_SECRET: self.oauth.client_secret
|
||||
}
|
||||
if not config_from_file(hass.config.path(FITBIT_CONFIG_FILE),
|
||||
config_contents):
|
||||
_LOGGER.error("Failed to save config file")
|
||||
|
@ -490,9 +503,11 @@ class FitbitSensor(Entity):
|
|||
if self.resource_type == 'activities/heart':
|
||||
self._state = response[container][-1]. \
|
||||
get('value').get('restingHeartRate')
|
||||
|
||||
token = self.client.client.session.token
|
||||
config_contents = {
|
||||
ATTR_ACCESS_TOKEN: self.client.client.token['access_token'],
|
||||
ATTR_REFRESH_TOKEN: self.client.client.token['refresh_token'],
|
||||
ATTR_ACCESS_TOKEN: token.get('access_token'),
|
||||
ATTR_REFRESH_TOKEN: token.get('refresh_token'),
|
||||
ATTR_CLIENT_ID: self.client.client.client_id,
|
||||
ATTR_CLIENT_SECRET: self.client.client.client_secret,
|
||||
ATTR_LAST_SAVED_AT: int(time.time())
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue