mirror of
https://github.com/home-assistant/core.git
synced 2025-08-04 21:25:13 +02:00
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.
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
Reference in New Issue
Block a user