Merge pull request #9327 from home-assistant/release-0-53

0.53
This commit is contained in:
Paulus Schoutsen 2017-09-09 00:31:53 -07:00 committed by GitHub
commit 2d72cff575
168 changed files with 8232 additions and 1551 deletions

View file

@ -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

View file

@ -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

View file

@ -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."""

View file

@ -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()

View file

@ -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
}

View file

@ -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."""

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View 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)

View file

@ -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({

View file

@ -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)

View file

@ -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()

View file

@ -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

View file

@ -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)

View 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()

View file

@ -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."""

View file

@ -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)

View file

@ -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")

View file

@ -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)

View 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)

View 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

View 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)

View 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')
}
}

View 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)

View 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']

View file

@ -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}

View 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)

View 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()

View 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()

View file

@ -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)

View file

@ -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

View file

@ -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."""

View file

@ -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])

View file

@ -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."""

View file

@ -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

View 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)

View 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
)

View file

@ -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,

View file

@ -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)

View file

@ -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')

View file

@ -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 &&

View file

@ -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

@ -1 +1 @@
Subproject commit 07d5d6e8a9205f77f83f68615695bcbc73cf83e3
Subproject commit 19187ce518190823a8ccc5e7fc3d262cd218fa74

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -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>

File diff suppressed because one or more lines are too long

View file

@ -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 : '');

View file

@ -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):

View file

@ -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

View file

@ -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__)

View file

@ -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__)

View 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()

View file

@ -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'

View file

@ -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__)

View file

@ -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)

View file

@ -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)

View file

@ -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')

View file

@ -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()

View file

@ -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)

View file

@ -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]

View file

@ -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])

View file

@ -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

View 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))

View file

@ -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)

View 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

View file

@ -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__)

View 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

View file

@ -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__)

View file

@ -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__)

View file

@ -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)

View file

@ -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

View file

@ -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:

View file

@ -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:

View 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)

View 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

View file

@ -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):

View file

@ -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."""

View file

@ -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'

View 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)

View 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")

View file

@ -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)

View file

@ -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__)

View file

@ -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."""

View file

@ -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()

View file

@ -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()

View 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)

View file

@ -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

View file

@ -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)

View 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

View file

@ -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