Merge remote-tracking branch 'upstream/dev' into nest_sensor

# Conflicts:
#	.coveragerc
#	requirements_all.txt
This commit is contained in:
Joseph Hughes 2016-01-14 11:00:34 -07:00
commit ac34db3c8a
75 changed files with 2993 additions and 810 deletions

View file

@ -36,6 +36,9 @@ omit =
homeassistant/components/rfxtrx.py homeassistant/components/rfxtrx.py
homeassistant/components/*/rfxtrx.py homeassistant/components/*/rfxtrx.py
homeassistant/components/mysensors.py
homeassistant/components/*/mysensors.py
homeassistant/components/binary_sensor/arest.py homeassistant/components/binary_sensor/arest.py
homeassistant/components/binary_sensor/nest.py homeassistant/components/binary_sensor/nest.py
homeassistant/components/binary_sensor/rest.py homeassistant/components/binary_sensor/rest.py
@ -74,6 +77,7 @@ omit =
homeassistant/components/media_player/plex.py homeassistant/components/media_player/plex.py
homeassistant/components/media_player/sonos.py homeassistant/components/media_player/sonos.py
homeassistant/components/media_player/squeezebox.py homeassistant/components/media_player/squeezebox.py
homeassistant/components/notify/free_mobile.py
homeassistant/components/notify/instapush.py homeassistant/components/notify/instapush.py
homeassistant/components/notify/nma.py homeassistant/components/notify/nma.py
homeassistant/components/notify/pushbullet.py homeassistant/components/notify/pushbullet.py
@ -93,8 +97,8 @@ omit =
homeassistant/components/sensor/eliqonline.py homeassistant/components/sensor/eliqonline.py
homeassistant/components/sensor/forecast.py homeassistant/components/sensor/forecast.py
homeassistant/components/sensor/glances.py homeassistant/components/sensor/glances.py
homeassistant/components/sensor/mysensors.py
homeassistant/components/sensor/nest.py homeassistant/components/sensor/nest.py
homeassistant/components/sensor/netatmo.py
homeassistant/components/sensor/openweathermap.py homeassistant/components/sensor/openweathermap.py
homeassistant/components/sensor/rest.py homeassistant/components/sensor/rest.py
homeassistant/components/sensor/rpi_gpio.py homeassistant/components/sensor/rpi_gpio.py

View file

@ -8,9 +8,7 @@ python:
- 3.4 - 3.4
- 3.5 - 3.5
install: install:
# Validate requirements_all.txt on Python 3.4 - "true"
- if [[ $TRAVIS_PYTHON_VERSION == '3.4' ]]; then python3 setup.py -q develop 2>/dev/null; tput setaf 1; script/gen_requirements_all.py validate; tput sgr0; fi
- script/bootstrap_server
script: script:
- script/cibuild - script/cibuild
matrix: matrix:

View file

@ -87,13 +87,21 @@ def setup(hass, config):
lambda item: util.split_entity_id(item)[0]) lambda item: util.split_entity_id(item)[0])
for domain, ent_ids in by_domain: for domain, ent_ids in by_domain:
# We want to block for all calls and only return when all calls
# have been processed. If a service does not exist it causes a 10
# second delay while we're blocking waiting for a response.
# But services can be registered on other HA instances that are
# listening to the bus too. So as a in between solution, we'll
# block only if the service is defined in the current HA instance.
blocking = hass.services.has_service(domain, service.service)
# Create a new dict for this call # Create a new dict for this call
data = dict(service.data) data = dict(service.data)
# ent_ids is a generator, convert it to a list. # ent_ids is a generator, convert it to a list.
data[ATTR_ENTITY_ID] = list(ent_ids) data[ATTR_ENTITY_ID] = list(ent_ids)
hass.services.call(domain, service.service, data, True) hass.services.call(domain, service.service, data, blocking)
hass.services.register(ha.DOMAIN, SERVICE_TURN_OFF, handle_turn_service) hass.services.register(ha.DOMAIN, SERVICE_TURN_OFF, handle_turn_service)
hass.services.register(ha.DOMAIN, SERVICE_TURN_ON, handle_turn_service) hass.services.register(ha.DOMAIN, SERVICE_TURN_ON, handle_turn_service)

View file

@ -68,7 +68,8 @@ class ManualAlarm(alarm.AlarmControlPanel):
@property @property
def state(self): def state(self):
""" Returns the state of the device. """ """ Returns the state of the device. """
if self._state in (STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY) and \ if self._state in (STATE_ALARM_ARMED_HOME,
STATE_ALARM_ARMED_AWAY) and \
self._pending_time and self._state_ts + self._pending_time > \ self._pending_time and self._state_ts + self._pending_time > \
dt_util.utcnow(): dt_util.utcnow():
return STATE_ALARM_PENDING return STATE_ALARM_PENDING

View file

@ -67,7 +67,7 @@ class VerisureAlarm(alarm.AlarmControlPanel):
self._state = STATE_ALARM_DISARMED self._state = STATE_ALARM_DISARMED
elif verisure.ALARM_STATUS[self._id].status == 'armedhome': elif verisure.ALARM_STATUS[self._id].status == 'armedhome':
self._state = STATE_ALARM_ARMED_HOME self._state = STATE_ALARM_ARMED_HOME
elif verisure.ALARM_STATUS[self._id].status == 'armedaway': elif verisure.ALARM_STATUS[self._id].status == 'armed':
self._state = STATE_ALARM_ARMED_AWAY self._state = STATE_ALARM_ARMED_AWAY
elif verisure.ALARM_STATUS[self._id].status != 'pending': elif verisure.ALARM_STATUS[self._id].status != 'pending':
_LOGGER.error( _LOGGER.error(

View file

@ -11,6 +11,7 @@ import logging
from homeassistant.const import HTTP_OK, HTTP_UNPROCESSABLE_ENTITY from homeassistant.const import HTTP_OK, HTTP_UNPROCESSABLE_ENTITY
from homeassistant.util import template from homeassistant.util import template
from homeassistant.helpers.service import call_from_config
DOMAIN = 'alexa' DOMAIN = 'alexa'
DEPENDENCIES = ['http'] DEPENDENCIES = ['http']
@ -23,6 +24,7 @@ API_ENDPOINT = '/api/alexa'
CONF_INTENTS = 'intents' CONF_INTENTS = 'intents'
CONF_CARD = 'card' CONF_CARD = 'card'
CONF_SPEECH = 'speech' CONF_SPEECH = 'speech'
CONF_ACTION = 'action'
def setup(hass, config): def setup(hass, config):
@ -80,6 +82,7 @@ def _handle_alexa(handler, path_match, data):
speech = config.get(CONF_SPEECH) speech = config.get(CONF_SPEECH)
card = config.get(CONF_CARD) card = config.get(CONF_CARD)
action = config.get(CONF_ACTION)
# pylint: disable=unsubscriptable-object # pylint: disable=unsubscriptable-object
if speech is not None: if speech is not None:
@ -89,6 +92,9 @@ def _handle_alexa(handler, path_match, data):
response.add_card(CardType[card['type']], card['title'], response.add_card(CardType[card['type']], card['title'],
card['content']) card['content'])
if action is not None:
call_from_config(handler.server.hass, action, True)
handler.write_json(response.as_dict()) handler.write_json(response.as_dict())

View file

@ -9,9 +9,9 @@ https://home-assistant.io/components/automation/
import logging import logging
from homeassistant.bootstrap import prepare_setup_platform from homeassistant.bootstrap import prepare_setup_platform
from homeassistant.util import split_entity_id from homeassistant.const import CONF_PLATFORM
from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM
from homeassistant.components import logbook from homeassistant.components import logbook
from homeassistant.helpers.service import call_from_config
DOMAIN = 'automation' DOMAIN = 'automation'
@ -19,8 +19,6 @@ DEPENDENCIES = ['group']
CONF_ALIAS = 'alias' CONF_ALIAS = 'alias'
CONF_SERVICE = 'service' CONF_SERVICE = 'service'
CONF_SERVICE_ENTITY_ID = 'entity_id'
CONF_SERVICE_DATA = 'data'
CONF_CONDITION = 'condition' CONF_CONDITION = 'condition'
CONF_ACTION = 'action' CONF_ACTION = 'action'
@ -96,22 +94,7 @@ def _get_action(hass, config, name):
_LOGGER.info('Executing %s', name) _LOGGER.info('Executing %s', name)
logbook.log_entry(hass, name, 'has been triggered', DOMAIN) logbook.log_entry(hass, name, 'has been triggered', DOMAIN)
domain, service = split_entity_id(config[CONF_SERVICE]) call_from_config(hass, config)
service_data = config.get(CONF_SERVICE_DATA, {})
if not isinstance(service_data, dict):
_LOGGER.error("%s should be a dictionary", CONF_SERVICE_DATA)
service_data = {}
if CONF_SERVICE_ENTITY_ID in config:
try:
service_data[ATTR_ENTITY_ID] = \
config[CONF_SERVICE_ENTITY_ID].split(",")
except AttributeError:
service_data[ATTR_ENTITY_ID] = \
config[CONF_SERVICE_ENTITY_ID]
hass.services.call(domain, service, service_data)
return action return action

View file

@ -6,6 +6,7 @@ Offers numeric state listening automation rules.
For more details about this automation rule, please refer to the documentation For more details about this automation rule, please refer to the documentation
at https://home-assistant.io/components/automation/#numeric-state-trigger at https://home-assistant.io/components/automation/#numeric-state-trigger
""" """
from functools import partial
import logging import logging
from homeassistant.const import CONF_VALUE_TEMPLATE from homeassistant.const import CONF_VALUE_TEMPLATE
@ -20,6 +21,14 @@ CONF_ABOVE = "above"
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
def _renderer(hass, value_template, state):
"""Render state value."""
if value_template is None:
return state.state
return template.render(hass, value_template, {'state': state})
def trigger(hass, config, action): def trigger(hass, config, action):
""" Listen for state changes based on `config`. """ """ Listen for state changes based on `config`. """
entity_id = config.get(CONF_ENTITY_ID) entity_id = config.get(CONF_ENTITY_ID)
@ -38,12 +47,7 @@ def trigger(hass, config, action):
CONF_BELOW, CONF_ABOVE) CONF_BELOW, CONF_ABOVE)
return False return False
if value_template is not None: renderer = partial(_renderer, hass, value_template)
renderer = lambda value: template.render(hass,
value_template,
{'state': value})
else:
renderer = lambda value: value.state
# pylint: disable=unused-argument # pylint: disable=unused-argument
def state_automation_listener(entity, from_s, to_s): def state_automation_listener(entity, from_s, to_s):
@ -79,12 +83,7 @@ def if_action(hass, config):
CONF_BELOW, CONF_ABOVE) CONF_BELOW, CONF_ABOVE)
return None return None
if value_template is not None: renderer = partial(_renderer, hass, value_template)
renderer = lambda value: template.render(hass,
value_template,
{'state': value})
else:
renderer = lambda value: value.state
def if_numeric_state(): def if_numeric_state():
""" Test numeric state condition. """ """ Test numeric state condition. """

View file

@ -80,23 +80,35 @@ def if_action(hass, config):
return None return None
if before is None: if before is None:
before_func = lambda: None def before_func():
"""Return no point in time."""
return None
elif before == EVENT_SUNRISE: elif before == EVENT_SUNRISE:
before_func = lambda: sun.next_rising_utc(hass) + before_offset def before_func():
"""Return time before sunrise."""
return sun.next_rising(hass) + before_offset
else: else:
before_func = lambda: sun.next_setting_utc(hass) + before_offset def before_func():
"""Return time before sunset."""
return sun.next_setting(hass) + before_offset
if after is None: if after is None:
after_func = lambda: None def after_func():
"""Return no point in time."""
return None
elif after == EVENT_SUNRISE: elif after == EVENT_SUNRISE:
after_func = lambda: sun.next_rising_utc(hass) + after_offset def after_func():
"""Return time after sunrise."""
return sun.next_rising(hass) + after_offset
else: else:
after_func = lambda: sun.next_setting_utc(hass) + after_offset def after_func():
"""Return time after sunset."""
return sun.next_setting(hass) + after_offset
def time_if(): def time_if():
""" Validate time based if-condition """ """ Validate time based if-condition """
now = dt_util.utcnow() now = dt_util.now()
before = before_func() before = before_func()
after = after_func() after = after_func()

View file

@ -32,8 +32,8 @@ def trigger(hass, config, action):
_error_time(config[CONF_AFTER], CONF_AFTER) _error_time(config[CONF_AFTER], CONF_AFTER)
return False return False
hours, minutes, seconds = after.hour, after.minute, after.second hours, minutes, seconds = after.hour, after.minute, after.second
elif (CONF_HOURS in config or CONF_MINUTES in config elif (CONF_HOURS in config or CONF_MINUTES in config or
or CONF_SECONDS in config): CONF_SECONDS in config):
hours = convert(config.get(CONF_HOURS), int) hours = convert(config.get(CONF_HOURS), int)
minutes = convert(config.get(CONF_MINUTES), int) minutes = convert(config.get(CONF_MINUTES), int)
seconds = convert(config.get(CONF_SECONDS), int) seconds = convert(config.get(CONF_SECONDS), int)

View file

@ -6,12 +6,11 @@ The rest binary sensor will consume responses sent by an exposed REST API.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.rest/ https://home-assistant.io/components/binary_sensor.rest/
""" """
from datetime import timedelta
import logging import logging
import requests
from homeassistant.const import CONF_VALUE_TEMPLATE from homeassistant.const import CONF_VALUE_TEMPLATE
from homeassistant.util import template, Throttle from homeassistant.util import template
from homeassistant.components.sensor.rest import RestData
from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.binary_sensor import BinarySensorDevice
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -19,60 +18,33 @@ _LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'REST Binary Sensor' DEFAULT_NAME = 'REST Binary Sensor'
DEFAULT_METHOD = 'GET' DEFAULT_METHOD = 'GET'
# Return cached results if last scan was less then this time ago
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
# pylint: disable=unused-variable # pylint: disable=unused-variable
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
""" Get the REST binary sensor. """ """Setup REST binary sensors."""
use_get = False
use_post = False
resource = config.get('resource', None) resource = config.get('resource', None)
method = config.get('method', DEFAULT_METHOD) method = config.get('method', DEFAULT_METHOD)
payload = config.get('payload', None) payload = config.get('payload', None)
verify_ssl = config.get('verify_ssl', True) verify_ssl = config.get('verify_ssl', True)
if method == 'GET': rest = RestData(method, resource, payload, verify_ssl)
use_get = True rest.update()
elif method == 'POST':
use_post = True
try: if rest.data is None:
if use_get: _LOGGER.error('Unable to fetch Rest data')
response = requests.get(resource, timeout=10, verify=verify_ssl)
elif use_post:
response = requests.post(resource, data=payload, timeout=10,
verify=verify_ssl)
if not response.ok:
_LOGGER.error("Response status is '%s'", response.status_code)
return False
except requests.exceptions.MissingSchema:
_LOGGER.error("Missing resource or schema in configuration. "
"Add http:// or https:// to your URL")
return False
except requests.exceptions.ConnectionError:
_LOGGER.error('No route to resource/endpoint: %s', resource)
return False return False
if use_get: add_devices([RestBinarySensor(
rest = RestDataGet(resource, verify_ssl) hass, rest, config.get('name', DEFAULT_NAME),
elif use_post: config.get(CONF_VALUE_TEMPLATE))])
rest = RestDataPost(resource, payload, verify_ssl)
add_devices([RestBinarySensor(hass,
rest,
config.get('name', DEFAULT_NAME),
config.get(CONF_VALUE_TEMPLATE))])
# pylint: disable=too-many-arguments # pylint: disable=too-many-arguments
class RestBinarySensor(BinarySensorDevice): class RestBinarySensor(BinarySensorDevice):
""" Implements a REST binary sensor. """ """REST binary sensor."""
def __init__(self, hass, rest, name, value_template): def __init__(self, hass, rest, name, value_template):
"""Initialize a REST binary sensor."""
self._hass = hass self._hass = hass
self.rest = rest self.rest = rest
self._name = name self._name = name
@ -82,63 +54,20 @@ class RestBinarySensor(BinarySensorDevice):
@property @property
def name(self): def name(self):
""" The name of the binary sensor. """ """Name of the binary sensor."""
return self._name return self._name
@property @property
def is_on(self): def is_on(self):
""" True if the binary sensor is on. """ """Return if the binary sensor is on."""
if self.rest.data is False: if self.rest.data is None:
return False return False
else:
if self._value_template is not None: if self._value_template is not None:
self.rest.data = template.render_with_possible_json_value( self.rest.data = template.render_with_possible_json_value(
self._hass, self._value_template, self.rest.data, False) self._hass, self._value_template, self.rest.data, False)
return bool(int(self.rest.data)) return bool(int(self.rest.data))
def update(self): def update(self):
""" Gets the latest data from REST API and updates the state. """ """Get the latest data from REST API and updates the state."""
self.rest.update() self.rest.update()
# pylint: disable=too-few-public-methods
class RestDataGet(object):
""" Class for handling the data retrieval with GET method. """
def __init__(self, resource, verify_ssl):
self._resource = resource
self._verify_ssl = verify_ssl
self.data = False
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
""" Gets the latest data from REST service with GET method. """
try:
response = requests.get(self._resource, timeout=10,
verify=self._verify_ssl)
self.data = response.text
except requests.exceptions.ConnectionError:
_LOGGER.error("No route to resource/endpoint: %s", self._resource)
self.data = False
# pylint: disable=too-few-public-methods
class RestDataPost(object):
""" Class for handling the data retrieval with POST method. """
def __init__(self, resource, payload, verify_ssl):
self._resource = resource
self._payload = payload
self._verify_ssl = verify_ssl
self.data = False
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
""" Gets the latest data from REST service with POST method. """
try:
response = requests.post(self._resource, data=self._payload,
timeout=10, verify=self._verify_ssl)
self.data = response.text
except requests.exceptions.ConnectionError:
_LOGGER.error("No route to resource/endpoint: %s", self._resource)
self.data = False

View file

@ -58,8 +58,8 @@ class AsusWrtDeviceScanner(object):
def __init__(self, config): def __init__(self, config):
self.host = config[CONF_HOST] self.host = config[CONF_HOST]
self.username = config[CONF_USERNAME] self.username = str(config[CONF_USERNAME])
self.password = config[CONF_PASSWORD] self.password = str(config[CONF_PASSWORD])
self.lock = threading.Lock() self.lock = threading.Lock()

View file

@ -19,7 +19,7 @@ from homeassistant.components.device_tracker import DOMAIN
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5) MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['pynetgear==0.3'] REQUIREMENTS = ['pynetgear==0.3.1']
def get_scanner(hass, config): def get_scanner(hass, config):

View file

@ -10,14 +10,17 @@ import json
import logging import logging
import homeassistant.components.mqtt as mqtt import homeassistant.components.mqtt as mqtt
from homeassistant.const import (STATE_HOME, STATE_NOT_HOME)
DEPENDENCIES = ['mqtt'] DEPENDENCIES = ['mqtt']
CONF_TRANSITION_EVENTS = 'use_events'
LOCATION_TOPIC = 'owntracks/+/+' LOCATION_TOPIC = 'owntracks/+/+'
EVENT_TOPIC = 'owntracks/+/+/event'
def setup_scanner(hass, config, see): def setup_scanner(hass, config, see):
""" Set up a OwnTracksks tracker. """ """ Set up an OwnTracks tracker. """
def owntracks_location_update(topic, payload, qos): def owntracks_location_update(topic, payload, qos):
""" MQTT message received. """ """ MQTT message received. """
@ -48,6 +51,56 @@ def setup_scanner(hass, config, see):
see(**kwargs) see(**kwargs)
mqtt.subscribe(hass, LOCATION_TOPIC, owntracks_location_update, 1) def owntracks_event_update(topic, payload, qos):
""" MQTT event (geofences) received. """
# Docs on available data:
# http://owntracks.org/booklet/tech/json/#_typetransition
try:
data = json.loads(payload)
except ValueError:
# If invalid JSON
logging.getLogger(__name__).error(
'Unable to parse payload as JSON: %s', payload)
return
if not isinstance(data, dict) or data.get('_type') != 'transition':
return
# check if in "home" fence or other zone
location = ''
if data['event'] == 'enter':
if data['desc'].lower() == 'home':
location = STATE_HOME
else:
location = data['desc']
elif data['event'] == 'leave':
location = STATE_NOT_HOME
else:
logging.getLogger(__name__).error(
'Misformatted mqtt msgs, _type=transition, event=%s',
data['event'])
return
parts = topic.split('/')
kwargs = {
'dev_id': '{}_{}'.format(parts[1], parts[2]),
'host_name': parts[1],
'gps': (data['lat'], data['lon']),
'location_name': location,
}
if 'acc' in data:
kwargs['gps_accuracy'] = data['acc']
see(**kwargs)
use_events = config.get(CONF_TRANSITION_EVENTS)
if use_events:
mqtt.subscribe(hass, EVENT_TOPIC, owntracks_event_update, 1)
else:
mqtt.subscribe(hass, LOCATION_TOPIC, owntracks_location_update, 1)
return True return True

View file

@ -0,0 +1,33 @@
# Describes the format for available device tracker services
see:
description: Control tracked device
fields:
mac:
description: MAC address of device
example: 'FF:FF:FF:FF:FF:FF'
dev_id:
description: Id of device (find id in known_devices.yaml)
example: 'phonedave'
host_name:
description: Hostname of device
example: 'Dave'
location_name:
description: Name of location where device is located (not_home is away)
example: 'home'
gps:
description: GPS coordinates where device is located (latitude, longitude)
example: '[51.509802, -0.086692]'
gps_accuracy:
description: Accuracy of GPS coordinates
example: '80'
battery:
description: Battery level of device
example: '100'

View file

@ -105,8 +105,7 @@ class SnmpScanner(object):
return return
if errstatus: if errstatus:
_LOGGER.error('SNMP error: %s at %s', errstatus.prettyPrint(), _LOGGER.error('SNMP error: %s at %s', errstatus.prettyPrint(),
errindex and restable[-1][int(errindex)-1] errindex and restable[-1][int(errindex)-1] or '?')
or '?')
return return
for resrow in restable: for resrow in restable:

View file

@ -242,8 +242,8 @@ class Tplink3DeviceScanner(TplinkDeviceScanner):
_LOGGER.info("Loading wireless clients...") _LOGGER.info("Loading wireless clients...")
url = 'http://{}/cgi-bin/luci/;stok={}/admin/wireless?form=statistics' \ url = ('http://{}/cgi-bin/luci/;stok={}/admin/wireless?'
.format(self.host, self.stok) 'form=statistics').format(self.host, self.stok)
referer = 'http://{}/webpages/index.html'.format(self.host) referer = 'http://{}/webpages/index.html'.format(self.host)
response = requests.post(url, response = requests.post(url,

View file

@ -1,2 +1,2 @@
""" DO NOT MODIFY. Auto-generated by build_frontend script """ """ DO NOT MODIFY. Auto-generated by build_frontend script """
VERSION = "be08c5a3ce12040bbdba2db83cb1a568" VERSION = "fe71771b9b24b0fb72a56e775c3e1112"

File diff suppressed because one or more lines are too long

@ -1 +1 @@
Subproject commit 50aadaf880a9cb36bf144540171ff5fa029e9eaf Subproject commit 0b99a5933c35b88c3369e992426fff8bf450aa01

View file

@ -198,12 +198,12 @@ class RequestHandler(SimpleHTTPRequestHandler):
"Error parsing JSON", HTTP_UNPROCESSABLE_ENTITY) "Error parsing JSON", HTTP_UNPROCESSABLE_ENTITY)
return return
self.authenticated = (self.server.api_password is None self.authenticated = (self.server.api_password is None or
or self.headers.get(HTTP_HEADER_HA_AUTH) == self.headers.get(HTTP_HEADER_HA_AUTH) ==
self.server.api_password self.server.api_password or
or data.get(DATA_API_PASSWORD) == data.get(DATA_API_PASSWORD) ==
self.server.api_password self.server.api_password or
or self.verify_session()) self.verify_session())
if '_METHOD' in data: if '_METHOD' in data:
method = data.pop('_METHOD') method = data.pop('_METHOD')

View file

@ -17,7 +17,7 @@ from urllib.parse import urlparse
from homeassistant.loader import get_component from homeassistant.loader import get_component
import homeassistant.util as util import homeassistant.util as util
import homeassistant.util.color as color_util import homeassistant.util.color as color_util
from homeassistant.const import CONF_HOST, DEVICE_DEFAULT_NAME from homeassistant.const import CONF_HOST, CONF_FILENAME, DEVICE_DEFAULT_NAME
from homeassistant.components.light import ( from homeassistant.components.light import (
Light, ATTR_BRIGHTNESS, ATTR_XY_COLOR, ATTR_COLOR_TEMP, Light, ATTR_BRIGHTNESS, ATTR_XY_COLOR, ATTR_COLOR_TEMP,
ATTR_TRANSITION, ATTR_FLASH, FLASH_LONG, FLASH_SHORT, ATTR_TRANSITION, ATTR_FLASH, FLASH_LONG, FLASH_SHORT,
@ -35,9 +35,9 @@ _CONFIGURING = {}
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
def _find_host_from_config(hass): def _find_host_from_config(hass, filename=PHUE_CONFIG_FILE):
""" Attempt to detect host based on existing configuration. """ """ Attempt to detect host based on existing configuration. """
path = hass.config.path(PHUE_CONFIG_FILE) path = hass.config.path(filename)
if not os.path.isfile(path): if not os.path.isfile(path):
return None return None
@ -54,13 +54,14 @@ def _find_host_from_config(hass):
def setup_platform(hass, config, add_devices_callback, discovery_info=None): def setup_platform(hass, config, add_devices_callback, discovery_info=None):
""" Gets the Hue lights. """ """ Gets the Hue lights. """
filename = config.get(CONF_FILENAME, PHUE_CONFIG_FILE)
if discovery_info is not None: if discovery_info is not None:
host = urlparse(discovery_info[1]).hostname host = urlparse(discovery_info[1]).hostname
else: else:
host = config.get(CONF_HOST, None) host = config.get(CONF_HOST, None)
if host is None: if host is None:
host = _find_host_from_config(hass) host = _find_host_from_config(hass, filename)
if host is None: if host is None:
_LOGGER.error('No host found in configuration') _LOGGER.error('No host found in configuration')
@ -70,17 +71,17 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
if host in _CONFIGURING: if host in _CONFIGURING:
return return
setup_bridge(host, hass, add_devices_callback) setup_bridge(host, hass, add_devices_callback, filename)
def setup_bridge(host, hass, add_devices_callback): def setup_bridge(host, hass, add_devices_callback, filename):
""" Setup a phue bridge based on host parameter. """ """ Setup a phue bridge based on host parameter. """
import phue import phue
try: try:
bridge = phue.Bridge( bridge = phue.Bridge(
host, host,
config_file_path=hass.config.path(PHUE_CONFIG_FILE)) config_file_path=hass.config.path(filename))
except ConnectionRefusedError: # Wrong host was given except ConnectionRefusedError: # Wrong host was given
_LOGGER.exception("Error connecting to the Hue bridge at %s", host) _LOGGER.exception("Error connecting to the Hue bridge at %s", host)
@ -89,7 +90,7 @@ def setup_bridge(host, hass, add_devices_callback):
except phue.PhueRegistrationException: except phue.PhueRegistrationException:
_LOGGER.warning("Connected to Hue at %s but not registered.", host) _LOGGER.warning("Connected to Hue at %s but not registered.", host)
request_configuration(host, hass, add_devices_callback) request_configuration(host, hass, add_devices_callback, filename)
return return
@ -121,10 +122,17 @@ def setup_bridge(host, hass, add_devices_callback):
new_lights = [] new_lights = []
api_name = api.get('config').get('name')
if api_name == 'RaspBee-GW':
bridge_type = 'deconz'
else:
bridge_type = 'hue'
for light_id, info in api_states.items(): for light_id, info in api_states.items():
if light_id not in lights: if light_id not in lights:
lights[light_id] = HueLight(int(light_id), info, lights[light_id] = HueLight(int(light_id), info,
bridge, update_lights) bridge, update_lights,
bridge_type=bridge_type)
new_lights.append(lights[light_id]) new_lights.append(lights[light_id])
else: else:
lights[light_id].info = info lights[light_id].info = info
@ -135,7 +143,7 @@ def setup_bridge(host, hass, add_devices_callback):
update_lights() update_lights()
def request_configuration(host, hass, add_devices_callback): def request_configuration(host, hass, add_devices_callback, filename):
""" Request configuration steps from the user. """ """ Request configuration steps from the user. """
configurator = get_component('configurator') configurator = get_component('configurator')
@ -149,7 +157,7 @@ def request_configuration(host, hass, add_devices_callback):
# pylint: disable=unused-argument # pylint: disable=unused-argument
def hue_configuration_callback(data): def hue_configuration_callback(data):
""" Actions to do when our configuration callback is called. """ """ Actions to do when our configuration callback is called. """
setup_bridge(host, hass, add_devices_callback) setup_bridge(host, hass, add_devices_callback, filename)
_CONFIGURING[host] = configurator.request_config( _CONFIGURING[host] = configurator.request_config(
hass, "Philips Hue", hue_configuration_callback, hass, "Philips Hue", hue_configuration_callback,
@ -163,11 +171,14 @@ def request_configuration(host, hass, add_devices_callback):
class HueLight(Light): class HueLight(Light):
""" Represents a Hue light """ """ Represents a Hue light """
def __init__(self, light_id, info, bridge, update_lights): # pylint: disable=too-many-arguments
def __init__(self, light_id, info, bridge, update_lights,
bridge_type='hue'):
self.light_id = light_id self.light_id = light_id
self.info = info self.info = info
self.bridge = bridge self.bridge = bridge
self.update_lights = update_lights self.update_lights = update_lights
self.bridge_type = bridge_type
@property @property
def unique_id(self): def unique_id(self):
@ -227,7 +238,7 @@ class HueLight(Light):
command['alert'] = 'lselect' command['alert'] = 'lselect'
elif flash == FLASH_SHORT: elif flash == FLASH_SHORT:
command['alert'] = 'select' command['alert'] = 'select'
else: elif self.bridge_type == 'hue':
command['alert'] = 'none' command['alert'] = 'none'
effect = kwargs.get(ATTR_EFFECT) effect = kwargs.get(ATTR_EFFECT)
@ -237,7 +248,7 @@ class HueLight(Light):
elif effect == EFFECT_RANDOM: elif effect == EFFECT_RANDOM:
command['hue'] = random.randrange(0, 65535) command['hue'] = random.randrange(0, 65535)
command['sat'] = random.randrange(150, 254) command['sat'] = random.randrange(150, 254)
else: elif self.bridge_type == 'hue':
command['effect'] = 'none' command['effect'] = 'none'
self.bridge.set_light(self.light_id, command) self.bridge.set_light(self.light_id, command)

View file

@ -13,8 +13,9 @@ from homeassistant.components.light import Light
from homeassistant.util import slugify from homeassistant.util import slugify
from homeassistant.const import ATTR_ENTITY_ID from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.components.rfxtrx import ATTR_STATE, ATTR_FIREEVENT, ATTR_PACKETID, \ from homeassistant.components.rfxtrx import (
ATTR_NAME, EVENT_BUTTON_PRESSED ATTR_STATE, ATTR_FIREEVENT, ATTR_PACKETID,
ATTR_NAME, EVENT_BUTTON_PRESSED)
DEPENDENCIES = ['rfxtrx'] DEPENDENCIES = ['rfxtrx']

View file

@ -7,16 +7,15 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.vera/ https://home-assistant.io/components/light.vera/
""" """
import logging import logging
import time
from requests.exceptions import RequestException from requests.exceptions import RequestException
from homeassistant.components.switch.vera import VeraSwitch from homeassistant.components.switch.vera import VeraSwitch
from homeassistant.components.light import ATTR_BRIGHTNESS from homeassistant.components.light import ATTR_BRIGHTNESS
REQUIREMENTS = ['https://github.com/pavoni/home-assistant-vera-api/archive/' from homeassistant.const import EVENT_HOMEASSISTANT_STOP, STATE_ON
'efdba4e63d58a30bc9b36d9e01e69858af9130b8.zip'
'#python-vera==0.1.1'] REQUIREMENTS = ['pyvera==0.2.3']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -36,10 +35,19 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
device_data = config.get('device_data', {}) device_data = config.get('device_data', {})
controller = veraApi.VeraController(base_url) vera_controller, created = veraApi.init_controller(base_url)
if created:
def stop_subscription(event):
""" Shutdown Vera subscriptions and subscription thread on exit"""
_LOGGER.info("Shutting down subscriptions.")
vera_controller.stop()
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_subscription)
devices = [] devices = []
try: try:
devices = controller.get_devices([ devices = vera_controller.get_devices([
'Switch', 'Switch',
'On/Off Switch', 'On/Off Switch',
'Dimmable Switch']) 'Dimmable Switch'])
@ -50,11 +58,11 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
lights = [] lights = []
for device in devices: for device in devices:
extra_data = device_data.get(device.deviceId, {}) extra_data = device_data.get(device.device_id, {})
exclude = extra_data.get('exclude', False) exclude = extra_data.get('exclude', False)
if exclude is not True: if exclude is not True:
lights.append(VeraLight(device, extra_data)) lights.append(VeraLight(device, vera_controller, extra_data))
add_devices_callback(lights) add_devices_callback(lights)
@ -77,5 +85,5 @@ class VeraLight(VeraSwitch):
else: else:
self.vera_device.switch_on() self.vera_device.switch_on()
self.last_command_send = time.time() self._state = STATE_ON
self.is_on_status = True self.update_ha_state()

View file

@ -72,6 +72,7 @@ SUPPORT_YOUTUBE = 64
SUPPORT_TURN_ON = 128 SUPPORT_TURN_ON = 128
SUPPORT_TURN_OFF = 256 SUPPORT_TURN_OFF = 256
SUPPORT_PLAY_MEDIA = 512 SUPPORT_PLAY_MEDIA = 512
SUPPORT_VOLUME_STEP = 1024
YOUTUBE_COVER_URL_FORMAT = 'https://img.youtube.com/vi/{}/1.jpg' YOUTUBE_COVER_URL_FORMAT = 'https://img.youtube.com/vi/{}/1.jpg'

View file

@ -20,7 +20,7 @@ from homeassistant.components.media_player import (
SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK,
MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO) MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO)
REQUIREMENTS = ['pychromecast==0.6.13'] REQUIREMENTS = ['pychromecast==0.6.14']
CONF_IGNORE_CEC = 'ignore_cec' CONF_IGNORE_CEC = 'ignore_cec'
CAST_SPLASH = 'https://home-assistant.io/images/cast/splash.png' CAST_SPLASH = 'https://home-assistant.io/images/cast/splash.png'
SUPPORT_CAST = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ SUPPORT_CAST = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \

View file

@ -35,7 +35,7 @@ SUPPORT_PLEX = SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK
def config_from_file(filename, config=None): def config_from_file(filename, config=None):
''' Small configuration file management function''' """ Small configuration file management function. """
if config: if config:
# We're writing configuration # We're writing configuration
try: try:
@ -85,7 +85,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
# pylint: disable=too-many-branches # pylint: disable=too-many-branches
def setup_plexserver(host, token, hass, add_devices_callback): def setup_plexserver(host, token, hass, add_devices_callback):
''' Setup a plexserver based on host parameter''' """ Setup a plexserver based on host parameter. """
import plexapi.server import plexapi.server
import plexapi.exceptions import plexapi.exceptions

View file

@ -22,9 +22,9 @@ from homeassistant.const import (
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
SUPPORT_SQUEEZEBOX = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE |\ SUPPORT_SQUEEZEBOX = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | \
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK |\ SUPPORT_VOLUME_MUTE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \
SUPPORT_TURN_ON | SUPPORT_TURN_OFF SUPPORT_SEEK | SUPPORT_TURN_ON | SUPPORT_TURN_OFF
# pylint: disable=unused-argument # pylint: disable=unused-argument
@ -202,11 +202,10 @@ class SqueezeBoxDevice(MediaPlayerDevice):
""" Image url of current playing media. """ """ Image url of current playing media. """
if 'artwork_url' in self._status: if 'artwork_url' in self._status:
return self._status['artwork_url'] return self._status['artwork_url']
return 'http://{server}:{port}/music/current/cover.jpg?player={player}'\ return ('http://{server}:{port}/music/current/cover.jpg?'
.format( 'player={player}').format(server=self._lms.host,
server=self._lms.host, port=self._lms.http_port,
port=self._lms.http_port, player=self._id)
player=self._id)
@property @property
def media_title(self): def media_title(self):

View file

@ -0,0 +1,438 @@
"""
homeassistant.components.media_player.universal
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Combines multiple media players into one for a universal controller.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.universal/
"""
# pylint: disable=import-error
from copy import copy
import logging
from homeassistant.helpers.event import track_state_change
from homeassistant.helpers.service import call_from_config
from homeassistant.const import (
STATE_IDLE, STATE_ON, STATE_OFF, CONF_NAME,
ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE,
SERVICE_TURN_OFF, SERVICE_TURN_ON,
SERVICE_VOLUME_UP, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_SET,
SERVICE_VOLUME_MUTE,
SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PAUSE,
SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_SEEK)
from homeassistant.components.media_player import (
MediaPlayerDevice, DOMAIN,
SUPPORT_VOLUME_STEP, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE,
SUPPORT_TURN_ON, SUPPORT_TURN_OFF,
SERVICE_PLAY_MEDIA, SERVICE_YOUTUBE_VIDEO,
ATTR_SUPPORTED_MEDIA_COMMANDS, ATTR_MEDIA_VOLUME_MUTED,
ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_DURATION,
ATTR_MEDIA_TITLE, ATTR_MEDIA_ARTIST, ATTR_MEDIA_ALBUM_NAME,
ATTR_MEDIA_TRACK, ATTR_MEDIA_SERIES_TITLE, ATTR_MEDIA_ALBUM_ARTIST,
ATTR_MEDIA_SEASON, ATTR_MEDIA_EPISODE, ATTR_MEDIA_CHANNEL,
ATTR_MEDIA_PLAYLIST, ATTR_APP_ID, ATTR_APP_NAME, ATTR_MEDIA_VOLUME_LEVEL,
ATTR_MEDIA_SEEK_POSITION)
ATTR_ACTIVE_CHILD = 'active_child'
CONF_ATTRS = 'attributes'
CONF_CHILDREN = 'children'
CONF_COMMANDS = 'commands'
CONF_PLATFORM = 'platform'
CONF_SERVICE = 'service'
CONF_SERVICE_DATA = 'service_data'
CONF_STATE = 'state'
OFF_STATES = [STATE_IDLE, STATE_OFF]
REQUIREMENTS = []
_LOGGER = logging.getLogger(__name__)
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
""" sets up the universal media players """
if not validate_config(config):
return
player = UniversalMediaPlayer(hass,
config[CONF_NAME],
config[CONF_CHILDREN],
config[CONF_COMMANDS],
config[CONF_ATTRS])
add_devices([player])
def validate_config(config):
""" validate universal media player configuration """
del config[CONF_PLATFORM]
# validate name
if CONF_NAME not in config:
_LOGGER.error('Universal Media Player configuration requires name')
return False
validate_children(config)
validate_commands(config)
validate_attributes(config)
del_keys = []
for key in config:
if key not in [CONF_NAME, CONF_CHILDREN, CONF_COMMANDS, CONF_ATTRS]:
_LOGGER.warning(
'Universal Media Player (%s) unrecognized parameter %s',
config[CONF_NAME], key)
del_keys.append(key)
for key in del_keys:
del config[key]
return True
def validate_children(config):
""" validate children """
if CONF_CHILDREN not in config:
_LOGGER.info(
'No children under Universal Media Player (%s)', config[CONF_NAME])
config[CONF_CHILDREN] = []
elif not isinstance(config[CONF_CHILDREN], list):
_LOGGER.warning(
'Universal Media Player (%s) children not list in config. '
'They will be ignored.',
config[CONF_NAME])
config[CONF_CHILDREN] = []
def validate_commands(config):
""" validate commands """
if CONF_COMMANDS not in config:
config[CONF_COMMANDS] = {}
elif not isinstance(config[CONF_COMMANDS], dict):
_LOGGER.warning(
'Universal Media Player (%s) specified commands not dict in '
'config. They will be ignored.',
config[CONF_NAME])
config[CONF_COMMANDS] = {}
def validate_attributes(config):
""" validate attributes """
if CONF_ATTRS not in config:
config[CONF_ATTRS] = {}
elif not isinstance(config[CONF_ATTRS], dict):
_LOGGER.warning(
'Universal Media Player (%s) specified attributes '
'not dict in config. They will be ignored.',
config[CONF_NAME])
config[CONF_ATTRS] = {}
for key, val in config[CONF_ATTRS].items():
attr = val.split('|', 1)
if len(attr) == 1:
attr.append(None)
config[CONF_ATTRS][key] = attr
class UniversalMediaPlayer(MediaPlayerDevice):
""" Represents a universal media player in HA """
# pylint: disable=too-many-public-methods
def __init__(self, hass, name, children, commands, attributes):
# pylint: disable=too-many-arguments
self.hass = hass
self._name = name
self._children = children
self._cmds = commands
self._attrs = attributes
self._child_state = None
def on_dependency_update(*_):
""" update ha state when dependencies update """
self.update_ha_state(True)
depend = copy(children)
for entity in attributes.values():
depend.append(entity[0])
track_state_change(hass, depend, on_dependency_update)
def _entity_lkp(self, entity_id, state_attr=None):
""" Looks up an entity state from hass """
state_obj = self.hass.states.get(entity_id)
if state_obj is None:
return
if state_attr:
return state_obj.attributes.get(state_attr)
return state_obj.state
def _override_or_child_attr(self, attr_name):
""" returns either the override or the active child for attr_name """
if attr_name in self._attrs:
return self._entity_lkp(self._attrs[attr_name][0],
self._attrs[attr_name][1])
return self._child_attr(attr_name)
def _child_attr(self, attr_name):
""" returns the active child's attr """
active_child = self._child_state
return active_child.attributes.get(attr_name) if active_child else None
def _call_service(self, service_name, service_data=None,
allow_override=False):
""" calls either a specified or active child's service """
if allow_override and service_name in self._cmds:
call_from_config(
self.hass, self._cmds[service_name], blocking=True)
return
if service_data is None:
service_data = {}
active_child = self._child_state
service_data[ATTR_ENTITY_ID] = active_child.entity_id
self.hass.services.call(DOMAIN, service_name, service_data,
blocking=True)
@property
def should_poll(self):
""" Indicates whether HA should poll for updates """
return False
@property
def master_state(self):
""" gets the master state from entity or none """
if CONF_STATE in self._attrs:
master_state = self._entity_lkp(self._attrs[CONF_STATE][0],
self._attrs[CONF_STATE][1])
return master_state if master_state else STATE_OFF
else:
return None
def _cache_active_child_state(self):
""" The state of the active child or None """
for child_name in self._children:
child_state = self.hass.states.get(child_name)
if child_state and child_state.state not in OFF_STATES:
self._child_state = child_state
return
self._child_state = None
@property
def name(self):
""" name of universal player """
return self._name
@property
def state(self):
"""
Current state of media player
Off if master state is off
ELSE Status of first active child
ELSE master state or off
"""
master_state = self.master_state # avoid multiple lookups
if master_state == STATE_OFF:
return STATE_OFF
active_child = self._child_state
if active_child:
return active_child.state
return master_state if master_state else STATE_OFF
@property
def volume_level(self):
""" Volume level of entity specified in attributes or active child """
return self._child_attr(ATTR_MEDIA_VOLUME_LEVEL)
@property
def is_volume_muted(self):
""" boolean if volume is muted """
return self._override_or_child_attr(ATTR_MEDIA_VOLUME_MUTED) \
in [True, STATE_ON]
@property
def media_content_id(self):
""" Content ID of current playing media. """
return self._child_attr(ATTR_MEDIA_CONTENT_ID)
@property
def media_content_type(self):
""" Content type of current playing media. """
return self._child_attr(ATTR_MEDIA_CONTENT_TYPE)
@property
def media_duration(self):
""" Duration of current playing media in seconds. """
return self._child_attr(ATTR_MEDIA_DURATION)
@property
def media_image_url(self):
""" Image url of current playing media. """
return self._child_attr(ATTR_ENTITY_PICTURE)
@property
def media_title(self):
""" Title of current playing media. """
return self._child_attr(ATTR_MEDIA_TITLE)
@property
def media_artist(self):
""" Artist of current playing media. (Music track only) """
return self._child_attr(ATTR_MEDIA_ARTIST)
@property
def media_album_name(self):
""" Album name of current playing media. (Music track only) """
return self._child_attr(ATTR_MEDIA_ALBUM_NAME)
@property
def media_album_artist(self):
""" Album arist of current playing media. (Music track only) """
return self._child_attr(ATTR_MEDIA_ALBUM_ARTIST)
@property
def media_track(self):
""" Track number of current playing media. (Music track only) """
return self._child_attr(ATTR_MEDIA_TRACK)
@property
def media_series_title(self):
""" Series title of current playing media. (TV Show only)"""
return self._child_attr(ATTR_MEDIA_SERIES_TITLE)
@property
def media_season(self):
""" Season of current playing media. (TV Show only) """
return self._child_attr(ATTR_MEDIA_SEASON)
@property
def media_episode(self):
""" Episode of current playing media. (TV Show only) """
return self._child_attr(ATTR_MEDIA_EPISODE)
@property
def media_channel(self):
""" Channel currently playing. """
return self._child_attr(ATTR_MEDIA_CHANNEL)
@property
def media_playlist(self):
""" Title of Playlist currently playing. """
return self._child_attr(ATTR_MEDIA_PLAYLIST)
@property
def app_id(self):
""" ID of the current running app. """
return self._child_attr(ATTR_APP_ID)
@property
def app_name(self):
""" Name of the current running app. """
return self._child_attr(ATTR_APP_NAME)
@property
def supported_media_commands(self):
""" Flags of media commands that are supported. """
flags = self._child_attr(ATTR_SUPPORTED_MEDIA_COMMANDS) or 0
if SERVICE_TURN_ON in self._cmds:
flags |= SUPPORT_TURN_ON
if SERVICE_TURN_OFF in self._cmds:
flags |= SUPPORT_TURN_OFF
if any([cmd in self._cmds for cmd in [SERVICE_VOLUME_UP,
SERVICE_VOLUME_DOWN]]):
flags |= SUPPORT_VOLUME_STEP
flags &= ~SUPPORT_VOLUME_SET
if SERVICE_VOLUME_MUTE in self._cmds and \
ATTR_MEDIA_VOLUME_MUTED in self._attrs:
flags |= SUPPORT_VOLUME_MUTE
return flags
@property
def device_state_attributes(self):
""" Extra attributes a device wants to expose. """
active_child = self._child_state
return {ATTR_ACTIVE_CHILD: active_child.entity_id} \
if active_child else {}
def turn_on(self):
""" turn the media player on. """
self._call_service(SERVICE_TURN_ON, allow_override=True)
def turn_off(self):
""" turn the media player off. """
self._call_service(SERVICE_TURN_OFF, allow_override=True)
def mute_volume(self, is_volume_muted):
""" mute the volume. """
data = {ATTR_MEDIA_VOLUME_MUTED: is_volume_muted}
self._call_service(SERVICE_VOLUME_MUTE, data, allow_override=True)
def set_volume_level(self, volume_level):
""" set volume level, range 0..1. """
data = {ATTR_MEDIA_VOLUME_LEVEL: volume_level}
self._call_service(SERVICE_VOLUME_SET, data)
def media_play(self):
""" Send play commmand. """
self._call_service(SERVICE_MEDIA_PLAY)
def media_pause(self):
""" Send pause command. """
self._call_service(SERVICE_MEDIA_PAUSE)
def media_previous_track(self):
""" Send previous track command. """
self._call_service(SERVICE_MEDIA_PREVIOUS_TRACK)
def media_next_track(self):
""" Send next track command. """
self._call_service(SERVICE_MEDIA_NEXT_TRACK)
def media_seek(self, position):
""" Send seek command. """
data = {ATTR_MEDIA_SEEK_POSITION: position}
self._call_service(SERVICE_MEDIA_SEEK, data)
def play_youtube(self, media_id):
""" Plays a YouTube media. """
data = {'media_id': media_id}
self._call_service(SERVICE_YOUTUBE_VIDEO, data)
def play_media(self, media_type, media_id):
""" Plays a piece of media. """
data = {'media_type': media_type, 'media_id': media_id}
self._call_service(SERVICE_PLAY_MEDIA, data)
def volume_up(self):
""" volume_up media player. """
self._call_service(SERVICE_VOLUME_UP, allow_override=True)
def volume_down(self):
""" volume_down media player. """
self._call_service(SERVICE_VOLUME_DOWN, allow_override=True)
def media_play_pause(self):
""" media_play_pause media player. """
self._call_service(SERVICE_MEDIA_PLAY_PAUSE)
def update(self):
""" event to trigger a state update in HA """
for child_name in self._children:
child_state = self.hass.states.get(child_name)
if child_state and child_state.state not in OFF_STATES:
self._child_state = child_state
return
self._child_state = None

View file

@ -149,9 +149,9 @@ class MQTT(object):
} }
if client_id is None: if client_id is None:
self._mqttc = mqtt.Client() self._mqttc = mqtt.Client(protocol=mqtt.MQTTv311)
else: else:
self._mqttc = mqtt.Client(client_id) self._mqttc = mqtt.Client(client_id, protocol=mqtt.MQTTv311)
self._mqttc.user_data_set(self.userdata) self._mqttc.user_data_set(self.userdata)

View file

@ -0,0 +1,114 @@
"""
homeassistant.components.mqtt_eventstream
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Connect two Home Assistant instances via mqtt.
Configuration:
To use the mqtt_eventstream component you will need to add the following to
your configuration.yaml file.
If you do not specify a publish_topic you will not forward events to the queue.
If you do not specify a subscribe_topic then you will not receive events from
the remote server.
mqtt_eventstream:
publish_topic: MyServerName
subscribe_topic: OtherHaServerName
"""
import json
from homeassistant.core import EventOrigin, State
from homeassistant.components.mqtt import DOMAIN as MQTT_DOMAIN
from homeassistant.components.mqtt import SERVICE_PUBLISH as MQTT_SVC_PUBLISH
from homeassistant.const import (
MATCH_ALL,
EVENT_TIME_CHANGED,
EVENT_CALL_SERVICE,
EVENT_SERVICE_EXECUTED,
EVENT_STATE_CHANGED,
)
import homeassistant.loader as loader
from homeassistant.remote import JSONEncoder
# The domain of your component. Should be equal to the name of your component
DOMAIN = "mqtt_eventstream"
# List of component names (string) your component depends upon
DEPENDENCIES = ['mqtt']
def setup(hass, config):
""" Setup our mqtt_eventstream component. """
mqtt = loader.get_component('mqtt')
pub_topic = config[DOMAIN].get('publish_topic', None)
sub_topic = config[DOMAIN].get('subscribe_topic', None)
def _event_publisher(event):
""" Handle events by publishing them on the mqtt queue. """
if event.origin != EventOrigin.local:
return
if event.event_type == EVENT_TIME_CHANGED:
return
# Filter out the events that were triggered by publishing
# to the MQTT topic, or you will end up in an infinite loop.
if event.event_type == EVENT_CALL_SERVICE:
if (
event.data.get('domain') == MQTT_DOMAIN and
event.data.get('service') == MQTT_SVC_PUBLISH and
event.data.get('topic') == pub_topic
):
return
# Filter out all the "event service executed" events because they
# are only used internally by core as callbacks for blocking
# during the interval while a service is being executed.
# They will serve no purpose to the external system,
# and thus are unnecessary traffic.
# And at any rate it would cause an infinite loop to publish them
# because publishing to an MQTT topic itself triggers one.
if event.event_type == EVENT_SERVICE_EXECUTED:
return
event_info = {'event_type': event.event_type, 'event_data': event.data}
msg = json.dumps(event_info, cls=JSONEncoder)
mqtt.publish(hass, pub_topic, msg)
# Only listen for local events if you are going to publish them
if pub_topic:
hass.bus.listen(MATCH_ALL, _event_publisher)
# Process events from a remote server that are received on a queue
def _event_receiver(topic, payload, qos):
"""
Receive events published by the other HA instance and fire
them on this hass instance.
"""
event = json.loads(payload)
event_type = event.get('event_type')
event_data = event.get('event_data')
# Special case handling for event STATE_CHANGED
# We will try to convert state dicts back to State objects
# Copied over from the _handle_api_post_events_event method
# of the api component.
if event_type == EVENT_STATE_CHANGED and event_data:
for key in ('old_state', 'new_state'):
state = State.from_dict(event_data.get(key))
if state:
event_data[key] = state
hass.bus.fire(
event_type,
event_data=event_data,
origin=EventOrigin.remote
)
# Only subscribe if you specified a topic
if sub_topic:
mqtt.subscribe(hass, sub_topic, _event_receiver)
hass.states.set('{domain}.initialized'.format(domain=DOMAIN), True)
# return boolean to indicate that initialization was successful
return True

View file

@ -0,0 +1,230 @@
"""
homeassistant.components.mysensors
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
MySensors component that connects to a MySensors gateway via pymysensors
API.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.mysensors.html
New features:
New MySensors component.
Updated MySensors Sensor platform.
New MySensors Switch platform. Currently only in optimistic mode (compare
with MQTT).
Multiple gateways are now supported.
Configuration.yaml:
mysensors:
gateways:
- port: '/dev/ttyUSB0'
persistence_file: 'path/mysensors.json'
- port: '/dev/ttyACM1'
persistence_file: 'path/mysensors2.json'
debug: true
persistence: true
version: '1.5'
"""
import logging
from homeassistant.helpers import validate_config
import homeassistant.bootstrap as bootstrap
from homeassistant.const import (
EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STOP,
EVENT_PLATFORM_DISCOVERED, ATTR_SERVICE, ATTR_DISCOVERED,
TEMP_CELCIUS,)
CONF_GATEWAYS = 'gateways'
CONF_PORT = 'port'
CONF_DEBUG = 'debug'
CONF_PERSISTENCE = 'persistence'
CONF_PERSISTENCE_FILE = 'persistence_file'
CONF_VERSION = 'version'
DEFAULT_VERSION = '1.4'
DOMAIN = 'mysensors'
DEPENDENCIES = []
REQUIREMENTS = [
'https://github.com/theolind/pymysensors/archive/'
'005bff4c5ca7a56acd30e816bc3bcdb5cb2d46fd.zip#pymysensors==0.4']
_LOGGER = logging.getLogger(__name__)
ATTR_NODE_ID = 'node_id'
ATTR_CHILD_ID = 'child_id'
ATTR_PORT = 'port'
GATEWAYS = None
SCAN_INTERVAL = 30
DISCOVER_SENSORS = "mysensors.sensors"
DISCOVER_SWITCHES = "mysensors.switches"
# Maps discovered services to their platforms
DISCOVERY_COMPONENTS = [
('sensor', DISCOVER_SENSORS),
('switch', DISCOVER_SWITCHES),
]
def setup(hass, config):
"""Setup the MySensors component."""
# pylint: disable=too-many-locals
if not validate_config(config,
{DOMAIN: [CONF_GATEWAYS]},
_LOGGER):
return False
import mysensors.mysensors as mysensors
version = str(config[DOMAIN].get(CONF_VERSION, DEFAULT_VERSION))
is_metric = (hass.config.temperature_unit == TEMP_CELCIUS)
def setup_gateway(port, persistence, persistence_file, version):
"""Return gateway after setup of the gateway."""
gateway = mysensors.SerialGateway(port, event_callback=None,
persistence=persistence,
persistence_file=persistence_file,
protocol_version=version)
gateway.metric = is_metric
gateway.debug = config[DOMAIN].get(CONF_DEBUG, False)
gateway = GatewayWrapper(gateway, version)
# pylint: disable=attribute-defined-outside-init
gateway.event_callback = gateway.callback_factory()
def gw_start(event):
"""Callback to trigger start of gateway and any persistence."""
gateway.start()
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP,
lambda event: gateway.stop())
if persistence:
for node_id in gateway.sensors:
gateway.event_callback('persistence', node_id)
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, gw_start)
return gateway
# Setup all ports from config
global GATEWAYS
GATEWAYS = {}
conf_gateways = config[DOMAIN][CONF_GATEWAYS]
if isinstance(conf_gateways, dict):
conf_gateways = [conf_gateways]
persistence = config[DOMAIN].get(CONF_PERSISTENCE, True)
for index, gway in enumerate(conf_gateways):
port = gway[CONF_PORT]
persistence_file = gway.get(
CONF_PERSISTENCE_FILE,
hass.config.path('mysensors{}.pickle'.format(index + 1)))
GATEWAYS[port] = setup_gateway(
port, persistence, persistence_file, version)
for (component, discovery_service) in DISCOVERY_COMPONENTS:
# Ensure component is loaded
if not bootstrap.setup_component(hass, component, config):
return False
# Fire discovery event
hass.bus.fire(EVENT_PLATFORM_DISCOVERED, {
ATTR_SERVICE: discovery_service,
ATTR_DISCOVERED: {}})
return True
def pf_callback_factory(
s_types, v_types, devices, add_devices, entity_class):
"""Return a new callback for the platform."""
def mysensors_callback(gateway, node_id):
"""Callback for mysensors platform."""
if gateway.sensors[node_id].sketch_name is None:
_LOGGER.info('No sketch_name: node %s', node_id)
return
# previously discovered, just update state with latest info
if node_id in devices:
for entity in devices[node_id]:
entity.update_ha_state(True)
return
# First time we see this node, detect sensors
for child in gateway.sensors[node_id].children.values():
name = '{} {}.{}'.format(
gateway.sensors[node_id].sketch_name, node_id, child.id)
for value_type in child.values.keys():
if child.type not in s_types or value_type not in v_types:
continue
devices[node_id].append(
entity_class(gateway, node_id, child.id, name, value_type))
if devices[node_id]:
_LOGGER.info('adding new devices: %s', devices[node_id])
add_devices(devices[node_id])
for entity in devices[node_id]:
entity.update_ha_state(True)
return mysensors_callback
class GatewayWrapper(object):
"""Gateway wrapper class, by subclassing serial gateway."""
def __init__(self, gateway, version):
"""Setup class attributes on instantiation.
Args:
gateway (mysensors.SerialGateway): Gateway to wrap.
version (str): Version of mysensors API.
Attributes:
_wrapped_gateway (mysensors.SerialGateway): Wrapped gateway.
version (str): Version of mysensors API.
platform_callbacks (list): Callback functions, one per platform.
const (module): Mysensors API constants.
__initialised (bool): True if GatewayWrapper is initialised.
"""
self._wrapped_gateway = gateway
self.version = version
self.platform_callbacks = []
self.const = self.get_const()
self.__initialised = True
def __getattr__(self, name):
"""See if this object has attribute name."""
# Do not use hasattr, it goes into infinite recurrsion
if name in self.__dict__:
# this object has it
return getattr(self, name)
# proxy to the wrapped object
return getattr(self._wrapped_gateway, name)
def __setattr__(self, name, value):
"""See if this object has attribute name then set to value."""
if '_GatewayWrapper__initialised' not in self.__dict__:
return object.__setattr__(self, name, value)
elif name in self.__dict__:
object.__setattr__(self, name, value)
else:
object.__setattr__(self._wrapped_gateway, name, value)
def get_const(self):
"""Get mysensors API constants."""
if self.version == '1.5':
import mysensors.const_15 as const
else:
import mysensors.const_14 as const
return const
def callback_factory(self):
"""Return a new callback function."""
def node_update(update_type, node_id):
"""Callback for node updates from the MySensors gateway."""
_LOGGER.info('update %s: node %s', update_type, node_id)
for callback in self.platform_callbacks:
callback(self, node_id)
return node_update

View file

@ -0,0 +1,51 @@
"""
homeassistant.components.notify.free_mobile
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Free Mobile SMS platform for notify component.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/notify.free_mobile/
"""
import logging
from homeassistant.helpers import validate_config
from homeassistant.components.notify import (
DOMAIN, BaseNotificationService)
from homeassistant.const import CONF_USERNAME, CONF_ACCESS_TOKEN
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['freesms==0.1.0']
def get_service(hass, config):
""" Get the Free Mobile SMS notification service. """
if not validate_config({DOMAIN: config},
{DOMAIN: [CONF_USERNAME,
CONF_ACCESS_TOKEN]},
_LOGGER):
return None
return FreeSMSNotificationService(config[CONF_USERNAME],
config[CONF_ACCESS_TOKEN])
# pylint: disable=too-few-public-methods
class FreeSMSNotificationService(BaseNotificationService):
""" Implements notification service for the Free Mobile SMS service. """
def __init__(self, username, access_token):
from freesms import FreeClient
self.free_client = FreeClient(username, access_token)
def send_message(self, message="", **kwargs):
""" Send a message to the Free Mobile user cell. """
resp = self.free_client.send_sms(message)
if resp.status_code == 400:
_LOGGER.error("At least one parameter is missing")
elif resp.status_code == 402:
_LOGGER.error("Too much SMS send in a few time")
elif resp.status_code == 403:
_LOGGER.error("Wrong Username/Password")
elif resp.status_code == 500:
_LOGGER.error("Server error, try later")

View file

@ -7,59 +7,62 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/notify.syslog/ https://home-assistant.io/components/notify.syslog/
""" """
import logging import logging
import syslog
from homeassistant.helpers import validate_config from homeassistant.helpers import validate_config
from homeassistant.components.notify import ( from homeassistant.components.notify import (
DOMAIN, ATTR_TITLE, BaseNotificationService) DOMAIN, ATTR_TITLE, BaseNotificationService)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
FACILITIES = {'kernel': syslog.LOG_KERN,
'user': syslog.LOG_USER,
'mail': syslog.LOG_MAIL,
'daemon': syslog.LOG_DAEMON,
'auth': syslog.LOG_KERN,
'LPR': syslog.LOG_LPR,
'news': syslog.LOG_NEWS,
'uucp': syslog.LOG_UUCP,
'cron': syslog.LOG_CRON,
'syslog': syslog.LOG_SYSLOG,
'local0': syslog.LOG_LOCAL0,
'local1': syslog.LOG_LOCAL1,
'local2': syslog.LOG_LOCAL2,
'local3': syslog.LOG_LOCAL3,
'local4': syslog.LOG_LOCAL4,
'local5': syslog.LOG_LOCAL5,
'local6': syslog.LOG_LOCAL6,
'local7': syslog.LOG_LOCAL7}
OPTIONS = {'pid': syslog.LOG_PID,
'cons': syslog.LOG_CONS,
'ndelay': syslog.LOG_NDELAY,
'nowait': syslog.LOG_NOWAIT,
'perror': syslog.LOG_PERROR}
PRIORITIES = {5: syslog.LOG_EMERG,
4: syslog.LOG_ALERT,
3: syslog.LOG_CRIT,
2: syslog.LOG_ERR,
1: syslog.LOG_WARNING,
0: syslog.LOG_NOTICE,
-1: syslog.LOG_INFO,
-2: syslog.LOG_DEBUG}
def get_service(hass, config): def get_service(hass, config):
""" Get the mail notification service. """ """Get the syslog notification service."""
if not validate_config({DOMAIN: config}, if not validate_config({DOMAIN: config},
{DOMAIN: ['facility', 'option', 'priority']}, {DOMAIN: ['facility', 'option', 'priority']},
_LOGGER): _LOGGER):
return None return None
_facility = FACILITIES.get(config['facility'], 40) import syslog
_option = OPTIONS.get(config['option'], 10)
_priority = PRIORITIES.get(config['priority'], -1) _facility = {
'kernel': syslog.LOG_KERN,
'user': syslog.LOG_USER,
'mail': syslog.LOG_MAIL,
'daemon': syslog.LOG_DAEMON,
'auth': syslog.LOG_KERN,
'LPR': syslog.LOG_LPR,
'news': syslog.LOG_NEWS,
'uucp': syslog.LOG_UUCP,
'cron': syslog.LOG_CRON,
'syslog': syslog.LOG_SYSLOG,
'local0': syslog.LOG_LOCAL0,
'local1': syslog.LOG_LOCAL1,
'local2': syslog.LOG_LOCAL2,
'local3': syslog.LOG_LOCAL3,
'local4': syslog.LOG_LOCAL4,
'local5': syslog.LOG_LOCAL5,
'local6': syslog.LOG_LOCAL6,
'local7': syslog.LOG_LOCAL7,
}.get(config['facility'], 40)
_option = {
'pid': syslog.LOG_PID,
'cons': syslog.LOG_CONS,
'ndelay': syslog.LOG_NDELAY,
'nowait': syslog.LOG_NOWAIT,
'perror': syslog.LOG_PERROR
}.get(config['option'], 10)
_priority = {
5: syslog.LOG_EMERG,
4: syslog.LOG_ALERT,
3: syslog.LOG_CRIT,
2: syslog.LOG_ERR,
1: syslog.LOG_WARNING,
0: syslog.LOG_NOTICE,
-1: syslog.LOG_INFO,
-2: syslog.LOG_DEBUG
}.get(config['priority'], -1)
return SyslogNotificationService(_facility, _option, _priority) return SyslogNotificationService(_facility, _option, _priority)
@ -76,6 +79,7 @@ class SyslogNotificationService(BaseNotificationService):
def send_message(self, message="", **kwargs): def send_message(self, message="", **kwargs):
""" Send a message to a user. """ """ Send a message to a user. """
import syslog
title = kwargs.get(ATTR_TITLE) title = kwargs.get(ATTR_TITLE)

View file

@ -9,8 +9,8 @@ https://home-assistant.io/components/sensor/
import logging import logging
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.components import (wink, zwave, isy994, from homeassistant.components import (
verisure, ecobee, tellduslive) wink, zwave, isy994, verisure, ecobee, tellduslive, mysensors)
DOMAIN = 'sensor' DOMAIN = 'sensor'
SCAN_INTERVAL = 30 SCAN_INTERVAL = 30
@ -25,6 +25,7 @@ DISCOVERY_PLATFORMS = {
verisure.DISCOVER_SENSORS: 'verisure', verisure.DISCOVER_SENSORS: 'verisure',
ecobee.DISCOVER_SENSORS: 'ecobee', ecobee.DISCOVER_SENSORS: 'ecobee',
tellduslive.DISCOVER_SENSORS: 'tellduslive', tellduslive.DISCOVER_SENSORS: 'tellduslive',
mysensors.DISCOVER_SENSORS: 'mysensors',
} }

View file

@ -11,8 +11,8 @@ import logging
import requests import requests
from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, \ from homeassistant.const import (ATTR_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE,
DEVICE_DEFAULT_NAME DEVICE_DEFAULT_NAME)
from homeassistant.exceptions import TemplateError from homeassistant.exceptions import TemplateError
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.util import template, Throttle from homeassistant.util import template, Throttle

View file

@ -70,5 +70,8 @@ class EliqSensor(Entity):
def update(self): def update(self):
""" Gets the latest data. """ """ Gets the latest data. """
response = self.api.get_data_now(channelid=self.channel_id) try:
self._state = int(response.power) response = self.api.get_data_now(channelid=self.channel_id)
self._state = int(response.power)
except TypeError: # raised by eliqonline library on any HTTP error
pass

View file

@ -7,150 +7,177 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.mysensors/ https://home-assistant.io/components/sensor.mysensors/
""" """
import logging import logging
from collections import defaultdict
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.const import ( from homeassistant.const import (
ATTR_BATTERY_LEVEL, EVENT_HOMEASSISTANT_STOP, ATTR_BATTERY_LEVEL,
TEMP_CELCIUS, TEMP_FAHRENHEIT, TEMP_CELCIUS, TEMP_FAHRENHEIT,
STATE_ON, STATE_OFF) STATE_ON, STATE_OFF)
CONF_PORT = "port" import homeassistant.components.mysensors as mysensors
CONF_DEBUG = "debug"
CONF_PERSISTENCE = "persistence"
CONF_PERSISTENCE_FILE = "persistence_file"
CONF_VERSION = "version"
ATTR_NODE_ID = "node_id"
ATTR_CHILD_ID = "child_id"
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['https://github.com/theolind/pymysensors/archive/' DEPENDENCIES = []
'd4b809c2167650691058d1e29bfd2c4b1792b4b0.zip'
'#pymysensors==0.3']
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
""" Setup the mysensors platform. """ """Setup the mysensors platform for sensors."""
# Only act if loaded via mysensors by discovery event.
# Otherwise gateway is not setup.
if discovery_info is None:
return
import mysensors.mysensors as mysensors for gateway in mysensors.GATEWAYS.values():
import mysensors.const_14 as const # Define the S_TYPES and V_TYPES that the platform should handle as
# states.
s_types = [
gateway.const.Presentation.S_TEMP,
gateway.const.Presentation.S_HUM,
gateway.const.Presentation.S_BARO,
gateway.const.Presentation.S_WIND,
gateway.const.Presentation.S_RAIN,
gateway.const.Presentation.S_UV,
gateway.const.Presentation.S_WEIGHT,
gateway.const.Presentation.S_POWER,
gateway.const.Presentation.S_DISTANCE,
gateway.const.Presentation.S_LIGHT_LEVEL,
gateway.const.Presentation.S_IR,
gateway.const.Presentation.S_WATER,
gateway.const.Presentation.S_AIR_QUALITY,
gateway.const.Presentation.S_CUSTOM,
gateway.const.Presentation.S_DUST,
gateway.const.Presentation.S_SCENE_CONTROLLER,
]
not_v_types = [
gateway.const.SetReq.V_ARMED,
gateway.const.SetReq.V_LIGHT,
gateway.const.SetReq.V_LOCK_STATUS,
]
if float(gateway.version) >= 1.5:
s_types.extend([
gateway.const.Presentation.S_COLOR_SENSOR,
gateway.const.Presentation.S_MULTIMETER,
])
not_v_types.extend([gateway.const.SetReq.V_STATUS, ])
v_types = [member for member in gateway.const.SetReq
if member.value not in not_v_types]
devices = {} # keep track of devices added to HA devices = defaultdict(list)
# Just assume celcius means that the user wants metric for now. gateway.platform_callbacks.append(mysensors.pf_callback_factory(
# It may make more sense to make this a global config option in the future. s_types, v_types, devices, add_devices, MySensorsSensor))
is_metric = (hass.config.temperature_unit == TEMP_CELCIUS)
def sensor_update(update_type, nid):
""" Callback for sensor updates from the MySensors gateway. """
_LOGGER.info("sensor_update %s: node %s", update_type, nid)
sensor = gateway.sensors[nid]
if sensor.sketch_name is None:
return
if nid not in devices:
devices[nid] = {}
node = devices[nid]
new_devices = []
for child_id, child in sensor.children.items():
if child_id not in node:
node[child_id] = {}
for value_type, value in child.values.items():
if value_type not in node[child_id]:
name = '{} {}.{}'.format(sensor.sketch_name, nid, child.id)
node[child_id][value_type] = \
MySensorsNodeValue(
nid, child_id, name, value_type, is_metric, const)
new_devices.append(node[child_id][value_type])
else:
node[child_id][value_type].update_sensor(
value, sensor.battery_level)
if new_devices:
_LOGGER.info("adding new devices: %s", new_devices)
add_devices(new_devices)
port = config.get(CONF_PORT)
if port is None:
_LOGGER.error("Missing required key 'port'")
return False
persistence = config.get(CONF_PERSISTENCE, True)
persistence_file = config.get(CONF_PERSISTENCE_FILE,
hass.config.path('mysensors.pickle'))
version = config.get(CONF_VERSION, '1.4')
gateway = mysensors.SerialGateway(port, sensor_update,
persistence=persistence,
persistence_file=persistence_file,
protocol_version=version)
gateway.metric = is_metric
gateway.debug = config.get(CONF_DEBUG, False)
gateway.start()
if persistence:
for nid in gateway.sensors:
sensor_update('sensor_update', nid)
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP,
lambda event: gateway.stop())
class MySensorsNodeValue(Entity): class MySensorsSensor(Entity):
""" Represents the value of a MySensors child node. """ """Represent the value of a MySensors child node."""
# pylint: disable=too-many-arguments, too-many-instance-attributes
def __init__(self, node_id, child_id, name, value_type, metric, const): # pylint: disable=too-many-arguments
self._name = name
def __init__(self, gateway, node_id, child_id, name, value_type):
"""Setup class attributes on instantiation.
Args:
gateway (GatewayWrapper): Gateway object.
node_id (str): Id of node.
child_id (str): Id of child.
name (str): Entity name.
value_type (str): Value type of child. Value is entity state.
Attributes:
gateway (GatewayWrapper): Gateway object.
node_id (str): Id of node.
child_id (str): Id of child.
_name (str): Entity name.
value_type (str): Value type of child. Value is entity state.
battery_level (int): Node battery level.
_values (dict): Child values. Non state values set as state attributes.
"""
self.gateway = gateway
self.node_id = node_id self.node_id = node_id
self.child_id = child_id self.child_id = child_id
self.battery_level = 0 self._name = name
self.value_type = value_type self.value_type = value_type
self.metric = metric self.battery_level = 0
self._value = '' self._values = {}
self.const = const
@property @property
def should_poll(self): def should_poll(self):
""" MySensor gateway pushes its state to HA. """ """MySensor gateway pushes its state to HA."""
return False return False
@property @property
def name(self): def name(self):
""" The name of this sensor. """ """The name of this entity."""
return self._name return self._name
@property @property
def state(self): def state(self):
""" Returns the state of the device. """ """Return the state of the device."""
return self._value if not self._values:
return ''
return self._values[self.value_type]
@property @property
def unit_of_measurement(self): def unit_of_measurement(self):
""" Unit of measurement of this entity. """ """Unit of measurement of this entity."""
if self.value_type == self.const.SetReq.V_TEMP: # pylint:disable=too-many-return-statements
return TEMP_CELCIUS if self.metric else TEMP_FAHRENHEIT if self.value_type == self.gateway.const.SetReq.V_TEMP:
elif self.value_type == self.const.SetReq.V_HUM or \ return TEMP_CELCIUS if self.gateway.metric else TEMP_FAHRENHEIT
self.value_type == self.const.SetReq.V_DIMMER or \ elif self.value_type == self.gateway.const.SetReq.V_HUM or \
self.value_type == self.const.SetReq.V_LIGHT_LEVEL: self.value_type == self.gateway.const.SetReq.V_DIMMER or \
self.value_type == self.gateway.const.SetReq.V_PERCENTAGE or \
self.value_type == self.gateway.const.SetReq.V_LIGHT_LEVEL:
return '%' return '%'
elif self.value_type == self.gateway.const.SetReq.V_WATT:
return 'W'
elif self.value_type == self.gateway.const.SetReq.V_KWH:
return 'kWh'
elif self.value_type == self.gateway.const.SetReq.V_VOLTAGE:
return 'V'
elif self.value_type == self.gateway.const.SetReq.V_CURRENT:
return 'A'
elif self.value_type == self.gateway.const.SetReq.V_IMPEDANCE:
return 'ohm'
elif self.gateway.const.SetReq.V_UNIT_PREFIX in self._values:
return self._values[self.gateway.const.SetReq.V_UNIT_PREFIX]
return None return None
@property
def device_state_attributes(self):
"""Return device specific state attributes."""
device_attr = dict(self._values)
device_attr.pop(self.value_type, None)
return device_attr
@property @property
def state_attributes(self): def state_attributes(self):
""" Returns the state attributes. """ """Return the state attributes."""
return { data = {
ATTR_NODE_ID: self.node_id, mysensors.ATTR_PORT: self.gateway.port,
ATTR_CHILD_ID: self.child_id, mysensors.ATTR_NODE_ID: self.node_id,
mysensors.ATTR_CHILD_ID: self.child_id,
ATTR_BATTERY_LEVEL: self.battery_level, ATTR_BATTERY_LEVEL: self.battery_level,
} }
def update_sensor(self, value, battery_level): device_attr = self.device_state_attributes
""" Update a sensor with the latest value from the controller. """
_LOGGER.info("%s value = %s", self._name, value) if device_attr is not None:
if self.value_type == self.const.SetReq.V_TRIPPED or \ data.update(device_attr)
self.value_type == self.const.SetReq.V_ARMED:
self._value = STATE_ON if int(value) == 1 else STATE_OFF return data
else:
self._value = value def update(self):
self.battery_level = battery_level """Update the controller with the latest values from a sensor."""
self.update_ha_state() node = self.gateway.sensors[self.node_id]
child = node.children[self.child_id]
for value_type, value in child.values.items():
_LOGGER.info(
"%s: value_type %s, value = %s", self._name, value_type, value)
if value_type == self.gateway.const.SetReq.V_TRIPPED:
self._values[value_type] = STATE_ON if int(
value) == 1 else STATE_OFF
else:
self._values[value_type] = value
self.battery_level = node.battery_level

View file

@ -0,0 +1,154 @@
"""
homeassistant.components.sensor.netatmo
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
NetAtmo Weather Service service.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.netatmo/
"""
import logging
from datetime import timedelta
from homeassistant.components.sensor import DOMAIN
from homeassistant.const import (CONF_API_KEY, CONF_USERNAME, CONF_PASSWORD,
TEMP_CELCIUS)
from homeassistant.helpers.entity import Entity
from homeassistant.helpers import validate_config
from homeassistant.util import Throttle
REQUIREMENTS = [
'https://github.com/HydrelioxGitHub/netatmo-api-python/archive/'
'43ff238a0122b0939a0dc4e8836b6782913fb6e2.zip'
'#lnetatmo==0.4.0']
_LOGGER = logging.getLogger(__name__)
SENSOR_TYPES = {
'temperature': ['Temperature', TEMP_CELCIUS],
'co2': ['CO2', 'ppm'],
'pressure': ['Pressure', 'mbar'],
'noise': ['Noise', 'dB'],
'humidity': ['Humidity', '%']
}
CONF_SECRET_KEY = 'secret_key'
ATTR_MODULE = 'modules'
# Return cached results if last scan was less then this time ago
# NetAtmo Data is uploaded to server every 10mn
# so this time should not be under
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=600)
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Get the NetAtmo sensor. """
if not validate_config({DOMAIN: config},
{DOMAIN: [CONF_API_KEY,
CONF_USERNAME,
CONF_PASSWORD,
CONF_SECRET_KEY]},
_LOGGER):
return None
import lnetatmo
authorization = lnetatmo.ClientAuth(config.get(CONF_API_KEY, None),
config.get(CONF_SECRET_KEY, None),
config.get(CONF_USERNAME, None),
config.get(CONF_PASSWORD, None))
if not authorization:
_LOGGER.error(
"Connection error "
"Please check your settings for NatAtmo API.")
return False
data = NetAtmoData(authorization)
dev = []
try:
# Iterate each module
for module_name, monitored_conditions in config[ATTR_MODULE].items():
# Test if module exist """
if module_name not in data.get_module_names():
_LOGGER.error('Module name: "%s" not found', module_name)
continue
# Only create sensor for monitored """
for variable in monitored_conditions:
if variable not in SENSOR_TYPES:
_LOGGER.error('Sensor type: "%s" does not exist', variable)
else:
dev.append(
NetAtmoSensor(data, module_name, variable))
except KeyError:
pass
add_devices(dev)
# pylint: disable=too-few-public-methods
class NetAtmoSensor(Entity):
""" Implements a NetAtmo sensor. """
def __init__(self, netatmo_data, module_name, sensor_type):
self._name = "NetAtmo {} {}".format(module_name,
SENSOR_TYPES[sensor_type][0])
self.netatmo_data = netatmo_data
self.module_name = module_name
self.type = sensor_type
self._state = None
self._unit_of_measurement = SENSOR_TYPES[sensor_type][1]
self.update()
@property
def name(self):
return self._name
@property
def state(self):
""" Returns the state of the device. """
return self._state
@property
def unit_of_measurement(self):
""" Unit of measurement of this entity, if any. """
return self._unit_of_measurement
# pylint: disable=too-many-branches
def update(self):
""" Gets the latest data from NetAtmo API and updates the states. """
self.netatmo_data.update()
data = self.netatmo_data.data[self.module_name]
if self.type == 'temperature':
self._state = round(data['Temperature'], 1)
elif self.type == 'humidity':
self._state = data['Humidity']
elif self.type == 'noise':
self._state = data['Noise']
elif self.type == 'co2':
self._state = data['CO2']
elif self.type == 'pressure':
self._state = round(data['Pressure'], 1)
class NetAtmoData(object):
""" Gets the latest data from NetAtmo. """
def __init__(self, auth):
self.auth = auth
self.data = None
def get_module_names(self):
""" Return all module available on the API as a list. """
self.update()
return self.data.keys()
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
""" Call the NetAtmo API to update the data. """
import lnetatmo
# Gets the latest data from NetAtmo. """
dev_list = lnetatmo.DeviceList(self.auth)
self.data = dev_list.lastData(exclude=3600)

View file

@ -13,14 +13,14 @@ from homeassistant.util import Throttle
from homeassistant.const import (CONF_API_KEY, TEMP_CELCIUS, TEMP_FAHRENHEIT) from homeassistant.const import (CONF_API_KEY, TEMP_CELCIUS, TEMP_FAHRENHEIT)
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['pyowm==2.2.1'] REQUIREMENTS = ['pyowm==2.3.0']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
SENSOR_TYPES = { SENSOR_TYPES = {
'weather': ['Condition', ''], 'weather': ['Condition', ''],
'temperature': ['Temperature', ''], 'temperature': ['Temperature', ''],
'wind_speed': ['Wind speed', 'm/s'], 'wind_speed': ['Wind speed', 'm/s'],
'humidity': ['Humidity', '%'], 'humidity': ['Humidity', '%'],
'pressure': ['Pressure', 'hPa'], 'pressure': ['Pressure', 'mbar'],
'clouds': ['Cloud coverage', '%'], 'clouds': ['Cloud coverage', '%'],
'rain': ['Rain', 'mm'], 'rain': ['Rain', 'mm'],
'snow': ['Snow', 'mm'] 'snow': ['Snow', 'mm']

View file

@ -26,47 +26,21 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
# pylint: disable=unused-variable # pylint: disable=unused-variable
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
""" Get the REST sensor. """ """ Get the REST sensor. """
use_get = False
use_post = False
resource = config.get('resource', None) resource = config.get('resource', None)
method = config.get('method', DEFAULT_METHOD) method = config.get('method', DEFAULT_METHOD)
payload = config.get('payload', None) payload = config.get('payload', None)
verify_ssl = config.get('verify_ssl', True) verify_ssl = config.get('verify_ssl', True)
if method == 'GET': rest = RestData(method, resource, payload, verify_ssl)
use_get = True rest.update()
elif method == 'POST':
use_post = True
try: if rest.data is None:
if use_get: _LOGGER.error('Unable to fetch Rest data')
response = requests.get(resource, timeout=10, verify=verify_ssl)
elif use_post:
response = requests.post(resource, data=payload, timeout=10,
verify=verify_ssl)
if not response.ok:
_LOGGER.error("Response status is '%s'", response.status_code)
return False
except requests.exceptions.MissingSchema:
_LOGGER.error("Missing resource or schema in configuration. "
"Add http:// or https:// to your URL")
return False
except requests.exceptions.ConnectionError:
_LOGGER.error("No route to resource/endpoint: %s", resource)
return False return False
if use_get: add_devices([RestSensor(
rest = RestDataGet(resource, verify_ssl) hass, rest, config.get('name', DEFAULT_NAME),
elif use_post: config.get('unit_of_measurement'), config.get(CONF_VALUE_TEMPLATE))])
rest = RestDataPost(resource, payload, verify_ssl)
add_devices([RestSensor(hass,
rest,
config.get('name', DEFAULT_NAME),
config.get('unit_of_measurement'),
config.get(CONF_VALUE_TEMPLATE))])
# pylint: disable=too-many-arguments # pylint: disable=too-many-arguments
@ -112,11 +86,11 @@ class RestSensor(Entity):
# pylint: disable=too-few-public-methods # pylint: disable=too-few-public-methods
class RestDataGet(object): class RestData(object):
""" Class for handling the data retrieval with GET method. """ """Class for handling the data retrieval."""
def __init__(self, resource, verify_ssl): def __init__(self, method, resource, data, verify_ssl):
self._resource = resource self._request = requests.Request(method, resource, data=data).prepare()
self._verify_ssl = verify_ssl self._verify_ssl = verify_ssl
self.data = None self.data = None
@ -124,31 +98,11 @@ class RestDataGet(object):
def update(self): def update(self):
""" Gets the latest data from REST service with GET method. """ """ Gets the latest data from REST service with GET method. """
try: try:
response = requests.get(self._resource, timeout=10, with requests.Session() as sess:
verify=self._verify_ssl) response = sess.send(self._request, timeout=10,
verify=self._verify_ssl)
self.data = response.text self.data = response.text
except requests.exceptions.ConnectionError: except requests.exceptions.RequestException:
_LOGGER.error("No route to resource/endpoint: %s", self._resource) _LOGGER.error("Error fetching data: %s", self._request)
self.data = None
# pylint: disable=too-few-public-methods
class RestDataPost(object):
""" Class for handling the data retrieval with POST method. """
def __init__(self, resource, payload, verify_ssl):
self._resource = resource
self._payload = payload
self._verify_ssl = verify_ssl
self.data = None
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
""" Gets the latest data from REST service with POST method. """
try:
response = requests.post(self._resource, data=self._payload,
timeout=10, verify=self._verify_ssl)
self.data = response.text
except requests.exceptions.ConnectionError:
_LOGGER.error("No route to resource/endpoint: %s", self._resource)
self.data = None self.data = None

View file

@ -22,10 +22,22 @@ DEPENDENCIES = ['tellduslive']
SENSOR_TYPE_TEMP = "temp" SENSOR_TYPE_TEMP = "temp"
SENSOR_TYPE_HUMIDITY = "humidity" SENSOR_TYPE_HUMIDITY = "humidity"
SENSOR_TYPE_RAINRATE = "rrate"
SENSOR_TYPE_RAINTOTAL = "rtot"
SENSOR_TYPE_WINDDIRECTION = "wdir"
SENSOR_TYPE_WINDAVERAGE = "wavg"
SENSOR_TYPE_WINDGUST = "wgust"
SENSOR_TYPE_WATT = "watt"
SENSOR_TYPES = { SENSOR_TYPES = {
SENSOR_TYPE_TEMP: ['Temperature', TEMP_CELCIUS, "mdi:thermometer"], SENSOR_TYPE_TEMP: ['Temperature', TEMP_CELCIUS, "mdi:thermometer"],
SENSOR_TYPE_HUMIDITY: ['Humidity', '%', "mdi:water"], SENSOR_TYPE_HUMIDITY: ['Humidity', '%', "mdi:water"],
SENSOR_TYPE_RAINRATE: ['Rain rate', 'mm', "mdi:water"],
SENSOR_TYPE_RAINTOTAL: ['Rain total', 'mm', "mdi:water"],
SENSOR_TYPE_WINDDIRECTION: ['Wind direction', '', ""],
SENSOR_TYPE_WINDAVERAGE: ['Wind average', 'm/s', ""],
SENSOR_TYPE_WINDGUST: ['Wind gust', 'm/s', ""],
SENSOR_TYPE_WATT: ['Watt', 'W', ""],
} }

View file

@ -13,11 +13,9 @@ import homeassistant.util.dt as dt_util
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.const import ( from homeassistant.const import (
ATTR_BATTERY_LEVEL, ATTR_TRIPPED, ATTR_ARMED, ATTR_LAST_TRIP_TIME, ATTR_BATTERY_LEVEL, ATTR_TRIPPED, ATTR_ARMED, ATTR_LAST_TRIP_TIME,
TEMP_CELCIUS, TEMP_FAHRENHEIT) TEMP_CELCIUS, TEMP_FAHRENHEIT, EVENT_HOMEASSISTANT_STOP)
REQUIREMENTS = ['https://github.com/pavoni/home-assistant-vera-api/archive/' REQUIREMENTS = ['pyvera==0.2.3']
'efdba4e63d58a30bc9b36d9e01e69858af9130b8.zip'
'#python-vera==0.1.1']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -37,7 +35,16 @@ def get_devices(hass, config):
device_data = config.get('device_data', {}) device_data = config.get('device_data', {})
vera_controller = veraApi.VeraController(base_url) vera_controller, created = veraApi.init_controller(base_url)
if created:
def stop_subscription(event):
""" Shutdown Vera subscriptions and subscription thread on exit"""
_LOGGER.info("Shutting down subscriptions.")
vera_controller.stop()
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_subscription)
categories = ['Temperature Sensor', 'Light Sensor', 'Sensor'] categories = ['Temperature Sensor', 'Light Sensor', 'Sensor']
devices = [] devices = []
try: try:
@ -49,11 +56,12 @@ def get_devices(hass, config):
vera_sensors = [] vera_sensors = []
for device in devices: for device in devices:
extra_data = device_data.get(device.deviceId, {}) extra_data = device_data.get(device.device_id, {})
exclude = extra_data.get('exclude', False) exclude = extra_data.get('exclude', False)
if exclude is not True: if exclude is not True:
vera_sensors.append(VeraSensor(device, extra_data)) vera_sensors.append(
VeraSensor(device, vera_controller, extra_data))
return vera_sensors return vera_sensors
@ -66,8 +74,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class VeraSensor(Entity): class VeraSensor(Entity):
""" Represents a Vera Sensor. """ """ Represents a Vera Sensor. """
def __init__(self, vera_device, extra_data=None): def __init__(self, vera_device, controller, extra_data=None):
self.vera_device = vera_device self.vera_device = vera_device
self.controller = controller
self.extra_data = extra_data self.extra_data = extra_data
if self.extra_data and self.extra_data.get('name'): if self.extra_data and self.extra_data.get('name'):
self._name = self.extra_data.get('name') self._name = self.extra_data.get('name')
@ -76,8 +85,14 @@ class VeraSensor(Entity):
self.current_value = '' self.current_value = ''
self._temperature_units = None self._temperature_units = None
self.controller.register(vera_device, self._update_callback)
def _update_callback(self, _device):
""" Called by the vera device callback to update state. """
self.update_ha_state(True)
def __str__(self): def __str__(self):
return "%s %s %s" % (self.name, self.vera_device.deviceId, self.state) return "%s %s %s" % (self.name, self.vera_device.device_id, self.state)
@property @property
def state(self): def state(self):
@ -100,26 +115,30 @@ class VeraSensor(Entity):
attr[ATTR_BATTERY_LEVEL] = self.vera_device.battery_level + '%' attr[ATTR_BATTERY_LEVEL] = self.vera_device.battery_level + '%'
if self.vera_device.is_armable: if self.vera_device.is_armable:
armed = self.vera_device.refresh_value('Armed') armed = self.vera_device.get_value('Armed')
attr[ATTR_ARMED] = 'True' if armed == '1' else 'False' attr[ATTR_ARMED] = 'True' if armed == '1' else 'False'
if self.vera_device.is_trippable: if self.vera_device.is_trippable:
last_tripped = self.vera_device.refresh_value('LastTrip') last_tripped = self.vera_device.get_value('LastTrip')
if last_tripped is not None: if last_tripped is not None:
utc_time = dt_util.utc_from_timestamp(int(last_tripped)) utc_time = dt_util.utc_from_timestamp(int(last_tripped))
attr[ATTR_LAST_TRIP_TIME] = dt_util.datetime_to_str( attr[ATTR_LAST_TRIP_TIME] = dt_util.datetime_to_str(
utc_time) utc_time)
else: else:
attr[ATTR_LAST_TRIP_TIME] = None attr[ATTR_LAST_TRIP_TIME] = None
tripped = self.vera_device.refresh_value('Tripped') tripped = self.vera_device.get_value('Tripped')
attr[ATTR_TRIPPED] = 'True' if tripped == '1' else 'False' attr[ATTR_TRIPPED] = 'True' if tripped == '1' else 'False'
attr['Vera Device Id'] = self.vera_device.vera_device_id attr['Vera Device Id'] = self.vera_device.vera_device_id
return attr return attr
@property
def should_poll(self):
""" Tells Home Assistant not to poll this entity. """
return False
def update(self): def update(self):
if self.vera_device.category == "Temperature Sensor": if self.vera_device.category == "Temperature Sensor":
self.vera_device.refresh_value('CurrentTemperature')
current_temp = self.vera_device.get_value('CurrentTemperature') current_temp = self.vera_device.get_value('CurrentTemperature')
vera_temp_units = self.vera_device.veraController.temperature_units vera_temp_units = self.vera_device.veraController.temperature_units
@ -137,10 +156,9 @@ class VeraSensor(Entity):
self.current_value = current_temp self.current_value = current_temp
elif self.vera_device.category == "Light Sensor": elif self.vera_device.category == "Light Sensor":
self.vera_device.refresh_value('CurrentLevel')
self.current_value = self.vera_device.get_value('CurrentLevel') self.current_value = self.vera_device.get_value('CurrentLevel')
elif self.vera_device.category == "Sensor": elif self.vera_device.category == "Sensor":
tripped = self.vera_device.refresh_value('Tripped') tripped = self.vera_device.get_value('Tripped')
self.current_value = 'Tripped' if tripped == '1' else 'Not Tripped' self.current_value = 'Tripped' if tripped == '1' else 'Not Tripped'
else: else:
self.current_value = 'Unknown' self.current_value = 'Unknown'

View file

@ -1,39 +1,10 @@
""" """
homeassistant.components.sensor.yr homeassistant.components.sensor.yr
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Yr.no weather service. Yr.no weather service.
Configuration: For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.yr/
Will show a symbol for the current weather as default:
sensor:
platform: yr
Will show temperatue and wind direction:
sensor:
platform: yr
monitored_conditions:
- temperature
- windDirection
Will show all available sensors:
sensor:
platform: yr
monitored_conditions:
- temperature
- symbol
- precipitation
- windSpeed
- pressure
- windDirection
- humidity
- fog
- cloudiness
- lowClouds
- mediumClouds
- highClouds
- dewpointTemperature
""" """
import logging import logging
@ -54,7 +25,7 @@ SENSOR_TYPES = {
'precipitation': ['Condition', 'mm'], 'precipitation': ['Condition', 'mm'],
'temperature': ['Temperature', '°C'], 'temperature': ['Temperature', '°C'],
'windSpeed': ['Wind speed', 'm/s'], 'windSpeed': ['Wind speed', 'm/s'],
'pressure': ['Pressure', 'hPa'], 'pressure': ['Pressure', 'mbar'],
'windDirection': ['Wind direction', '°'], 'windDirection': ['Wind direction', '°'],
'humidity': ['Humidity', '%'], 'humidity': ['Humidity', '%'],
'fog': ['Fog', '%'], 'fog': ['Fog', '%'],
@ -67,7 +38,7 @@ SENSOR_TYPES = {
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
""" Get the yr.no sensor. """ """ Get the Yr.no sensor. """
if None in (hass.config.latitude, hass.config.longitude): if None in (hass.config.latitude, hass.config.longitude):
_LOGGER.error("Latitude or longitude not set in Home Assistant config") _LOGGER.error("Latitude or longitude not set in Home Assistant config")

View file

@ -74,6 +74,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
value.type == zwave.TYPE_DECIMAL): value.type == zwave.TYPE_DECIMAL):
add_devices([ZWaveMultilevelSensor(value)]) add_devices([ZWaveMultilevelSensor(value)])
elif value.command_class == zwave.COMMAND_CLASS_ALARM:
add_devices([ZWaveAlarmSensor(value)])
class ZWaveSensor(Entity): class ZWaveSensor(Entity):
""" Represents a Z-Wave sensor. """ """ Represents a Z-Wave sensor. """
@ -216,3 +219,19 @@ class ZWaveMultilevelSensor(ZWaveSensor):
return TEMP_FAHRENHEIT return TEMP_FAHRENHEIT
else: else:
return unit return unit
class ZWaveAlarmSensor(ZWaveSensor):
""" A Z-wave sensor that sends Alarm alerts
Examples include certain Multisensors that have motion and
vibration capabilities. Z-Wave defines various alarm types
such as Smoke, Flood, Burglar, CarbonMonoxide, etc.
This wraps these alarms and allows you to use them to
trigger things, etc.
COMMAND_CLASS_ALARM is what we get here.
"""
# Empty subclass for now. Allows for later customizations
pass

View file

@ -18,7 +18,6 @@ from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['astral==0.8.1'] REQUIREMENTS = ['astral==0.8.1']
DOMAIN = "sun" DOMAIN = "sun"
ENTITY_ID = "sun.sun" ENTITY_ID = "sun.sun"
ENTITY_ID_ELEVATION = "sun.elevation"
CONF_ELEVATION = 'elevation' CONF_ELEVATION = 'elevation'
@ -33,21 +32,21 @@ _LOGGER = logging.getLogger(__name__)
def is_on(hass, entity_id=None): def is_on(hass, entity_id=None):
""" Returns if the sun is currently up based on the statemachine. """ """Test if the sun is currently up based on the statemachine."""
entity_id = entity_id or ENTITY_ID entity_id = entity_id or ENTITY_ID
return hass.states.is_state(entity_id, STATE_ABOVE_HORIZON) return hass.states.is_state(entity_id, STATE_ABOVE_HORIZON)
def next_setting(hass, entity_id=None): def next_setting(hass, entity_id=None):
""" Returns the local datetime object of the next sun setting. """ """Local datetime object of the next sun setting."""
utc_next = next_setting_utc(hass, entity_id) utc_next = next_setting_utc(hass, entity_id)
return dt_util.as_local(utc_next) if utc_next else None return dt_util.as_local(utc_next) if utc_next else None
def next_setting_utc(hass, entity_id=None): def next_setting_utc(hass, entity_id=None):
""" Returns the UTC datetime object of the next sun setting. """ """UTC datetime object of the next sun setting."""
entity_id = entity_id or ENTITY_ID entity_id = entity_id or ENTITY_ID
state = hass.states.get(ENTITY_ID) state = hass.states.get(ENTITY_ID)
@ -62,14 +61,14 @@ def next_setting_utc(hass, entity_id=None):
def next_rising(hass, entity_id=None): def next_rising(hass, entity_id=None):
""" Returns the local datetime object of the next sun rising. """ """Local datetime object of the next sun rising."""
utc_next = next_rising_utc(hass, entity_id) utc_next = next_rising_utc(hass, entity_id)
return dt_util.as_local(utc_next) if utc_next else None return dt_util.as_local(utc_next) if utc_next else None
def next_rising_utc(hass, entity_id=None): def next_rising_utc(hass, entity_id=None):
""" Returns the UTC datetime object of the next sun rising. """ """UTC datetime object of the next sun rising."""
entity_id = entity_id or ENTITY_ID entity_id = entity_id or ENTITY_ID
state = hass.states.get(ENTITY_ID) state = hass.states.get(ENTITY_ID)
@ -84,7 +83,7 @@ def next_rising_utc(hass, entity_id=None):
def setup(hass, config): def setup(hass, config):
""" Tracks the state of the sun. """ """Track the state of the sun in HA."""
if None in (hass.config.latitude, hass.config.longitude): if None in (hass.config.latitude, hass.config.longitude):
_LOGGER.error("Latitude or longitude not set in Home Assistant config") _LOGGER.error("Latitude or longitude not set in Home Assistant config")
return False return False
@ -125,7 +124,7 @@ def setup(hass, config):
class Sun(Entity): class Sun(Entity):
""" Represents the Sun. """ """Represents the Sun."""
entity_id = ENTITY_ID entity_id = ENTITY_ID
@ -158,12 +157,12 @@ class Sun(Entity):
@property @property
def next_change(self): def next_change(self):
""" Returns the datetime when the next change to the state is. """ """Datetime when the next change to the state is."""
return min(self.next_rising, self.next_setting) return min(self.next_rising, self.next_setting)
@property @property
def solar_elevation(self): def solar_elevation(self):
""" Returns the angle the sun is above the horizon""" """Angle the sun is above the horizon."""
from astral import Astral from astral import Astral
return Astral().solar_elevation( return Astral().solar_elevation(
dt_util.utcnow(), dt_util.utcnow(),

View file

@ -17,7 +17,7 @@ from homeassistant.helpers.entity import ToggleEntity
from homeassistant.const import ( from homeassistant.const import (
STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID) STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID)
from homeassistant.components import ( from homeassistant.components import (
group, discovery, wink, isy994, verisure, zwave, tellduslive) group, discovery, wink, isy994, verisure, zwave, tellduslive, mysensors)
DOMAIN = 'switch' DOMAIN = 'switch'
SCAN_INTERVAL = 30 SCAN_INTERVAL = 30
@ -41,6 +41,7 @@ DISCOVERY_PLATFORMS = {
verisure.DISCOVER_SWITCHES: 'verisure', verisure.DISCOVER_SWITCHES: 'verisure',
zwave.DISCOVER_SWITCHES: 'zwave', zwave.DISCOVER_SWITCHES: 'zwave',
tellduslive.DISCOVER_SWITCHES: 'tellduslive', tellduslive.DISCOVER_SWITCHES: 'tellduslive',
mysensors.DISCOVER_SWITCHES: 'mysensors',
} }
PROP_TO_ATTR = { PROP_TO_ATTR = {

View file

@ -0,0 +1,164 @@
"""
homeassistant.components.switch.mysensors
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Support for MySensors switches.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.mysensors.html
"""
import logging
from collections import defaultdict
from homeassistant.components.switch import SwitchDevice
from homeassistant.const import (
ATTR_BATTERY_LEVEL,
STATE_ON, STATE_OFF)
import homeassistant.components.mysensors as mysensors
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = []
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the mysensors platform for switches."""
# Only act if loaded via mysensors by discovery event.
# Otherwise gateway is not setup.
if discovery_info is None:
return
for gateway in mysensors.GATEWAYS.values():
# Define the S_TYPES and V_TYPES that the platform should handle as
# states.
s_types = [
gateway.const.Presentation.S_DOOR,
gateway.const.Presentation.S_MOTION,
gateway.const.Presentation.S_SMOKE,
gateway.const.Presentation.S_LIGHT,
gateway.const.Presentation.S_LOCK,
]
v_types = [
gateway.const.SetReq.V_ARMED,
gateway.const.SetReq.V_LIGHT,
gateway.const.SetReq.V_LOCK_STATUS,
]
if float(gateway.version) >= 1.5:
s_types.extend([
gateway.const.Presentation.S_BINARY,
gateway.const.Presentation.S_SPRINKLER,
gateway.const.Presentation.S_WATER_LEAK,
gateway.const.Presentation.S_SOUND,
gateway.const.Presentation.S_VIBRATION,
gateway.const.Presentation.S_MOISTURE,
])
v_types.extend([gateway.const.SetReq.V_STATUS, ])
devices = defaultdict(list)
gateway.platform_callbacks.append(mysensors.pf_callback_factory(
s_types, v_types, devices, add_devices, MySensorsSwitch))
class MySensorsSwitch(SwitchDevice):
"""Represent the value of a MySensors child node."""
# pylint: disable=too-many-arguments
def __init__(self, gateway, node_id, child_id, name, value_type):
"""Setup class attributes on instantiation.
Args:
gateway (GatewayWrapper): Gateway object.
node_id (str): Id of node.
child_id (str): Id of child.
name (str): Entity name.
value_type (str): Value type of child. Value is entity state.
Attributes:
gateway (GatewayWrapper): Gateway object
node_id (str): Id of node.
child_id (str): Id of child.
_name (str): Entity name.
value_type (str): Value type of child. Value is entity state.
battery_level (int): Node battery level.
_values (dict): Child values. Non state values set as state attributes.
"""
self.gateway = gateway
self.node_id = node_id
self.child_id = child_id
self._name = name
self.value_type = value_type
self.battery_level = 0
self._values = {}
@property
def should_poll(self):
"""MySensor gateway pushes its state to HA."""
return False
@property
def name(self):
"""The name of this entity."""
return self._name
@property
def device_state_attributes(self):
"""Return device specific state attributes."""
device_attr = dict(self._values)
device_attr.pop(self.value_type, None)
return device_attr
@property
def state_attributes(self):
"""Return the state attributes."""
data = {
mysensors.ATTR_PORT: self.gateway.port,
mysensors.ATTR_NODE_ID: self.node_id,
mysensors.ATTR_CHILD_ID: self.child_id,
ATTR_BATTERY_LEVEL: self.battery_level,
}
device_attr = self.device_state_attributes
if device_attr is not None:
data.update(device_attr)
return data
@property
def is_on(self):
"""Return True if switch is on."""
if self.value_type in self._values:
return self._values[self.value_type] == STATE_ON
return False
def turn_on(self):
"""Turn the switch on."""
self.gateway.set_child_value(
self.node_id, self.child_id, self.value_type, 1)
self._values[self.value_type] = STATE_ON
self.update_ha_state()
def turn_off(self):
"""Turn the switch off."""
self.gateway.set_child_value(
self.node_id, self.child_id, self.value_type, 0)
self._values[self.value_type] = STATE_OFF
self.update_ha_state()
def update(self):
"""Update the controller with the latest value from a sensor."""
node = self.gateway.sensors[self.node_id]
child = node.children[self.child_id]
for value_type, value in child.values.items():
_LOGGER.info(
"%s: value_type %s, value = %s", self._name, value_type, value)
if value_type == self.gateway.const.SetReq.V_ARMED or \
value_type == self.gateway.const.SetReq.V_STATUS or \
value_type == self.gateway.const.SetReq.V_LIGHT or \
value_type == self.gateway.const.SetReq.V_LOCK_STATUS:
self._values[value_type] = (
STATE_ON if int(value) == 1 else STATE_OFF)
else:
self._values[value_type] = value
self.battery_level = node.battery_level

View file

@ -13,8 +13,9 @@ from homeassistant.components.switch import SwitchDevice
from homeassistant.util import slugify from homeassistant.util import slugify
from homeassistant.const import ATTR_ENTITY_ID from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.components.rfxtrx import ATTR_STATE, ATTR_FIREEVENT, ATTR_PACKETID, \ from homeassistant.components.rfxtrx import (
ATTR_NAME, EVENT_BUTTON_PRESSED ATTR_STATE, ATTR_FIREEVENT, ATTR_PACKETID,
ATTR_NAME, EVENT_BUTTON_PRESSED)
DEPENDENCIES = ['rfxtrx'] DEPENDENCIES = ['rfxtrx']

View file

@ -7,17 +7,21 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/switch.vera/ https://home-assistant.io/components/switch.vera/
""" """
import logging import logging
import time
from requests.exceptions import RequestException from requests.exceptions import RequestException
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from homeassistant.helpers.entity import ToggleEntity from homeassistant.components.switch import SwitchDevice
from homeassistant.const import (
ATTR_BATTERY_LEVEL, ATTR_TRIPPED, ATTR_ARMED, ATTR_LAST_TRIP_TIME)
REQUIREMENTS = ['https://github.com/pavoni/home-assistant-vera-api/archive/' from homeassistant.const import (
'efdba4e63d58a30bc9b36d9e01e69858af9130b8.zip' ATTR_BATTERY_LEVEL,
'#python-vera==0.1.1'] ATTR_TRIPPED,
ATTR_ARMED,
ATTR_LAST_TRIP_TIME,
EVENT_HOMEASSISTANT_STOP,
STATE_ON,
STATE_OFF)
REQUIREMENTS = ['pyvera==0.2.3']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -37,7 +41,16 @@ def get_devices(hass, config):
device_data = config.get('device_data', {}) device_data = config.get('device_data', {})
vera_controller = veraApi.VeraController(base_url) vera_controller, created = veraApi.init_controller(base_url)
if created:
def stop_subscription(event):
""" Shutdown Vera subscriptions and subscription thread on exit"""
_LOGGER.info("Shutting down subscriptions.")
vera_controller.stop()
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_subscription)
devices = [] devices = []
try: try:
devices = vera_controller.get_devices([ devices = vera_controller.get_devices([
@ -49,11 +62,12 @@ def get_devices(hass, config):
vera_switches = [] vera_switches = []
for device in devices: for device in devices:
extra_data = device_data.get(device.deviceId, {}) extra_data = device_data.get(device.device_id, {})
exclude = extra_data.get('exclude', False) exclude = extra_data.get('exclude', False)
if exclude is not True: if exclude is not True:
vera_switches.append(VeraSwitch(device, extra_data)) vera_switches.append(
VeraSwitch(device, vera_controller, extra_data))
return vera_switches return vera_switches
@ -63,19 +77,28 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
add_devices(get_devices(hass, config)) add_devices(get_devices(hass, config))
class VeraSwitch(ToggleEntity): class VeraSwitch(SwitchDevice):
""" Represents a Vera Switch. """ """ Represents a Vera Switch. """
def __init__(self, vera_device, extra_data=None): def __init__(self, vera_device, controller, extra_data=None):
self.vera_device = vera_device self.vera_device = vera_device
self.extra_data = extra_data self.extra_data = extra_data
self.controller = controller
if self.extra_data and self.extra_data.get('name'): if self.extra_data and self.extra_data.get('name'):
self._name = self.extra_data.get('name') self._name = self.extra_data.get('name')
else: else:
self._name = self.vera_device.name self._name = self.vera_device.name
self.is_on_status = False self._state = STATE_OFF
# for debouncing status check after command is sent
self.last_command_send = 0 self.controller.register(vera_device, self._update_callback)
def _update_callback(self, _device):
""" Called by the vera device callback to update state. """
if self.vera_device.is_switched_on():
self._state = STATE_ON
else:
self._state = STATE_OFF
self.update_ha_state()
@property @property
def name(self): def name(self):
@ -90,18 +113,18 @@ class VeraSwitch(ToggleEntity):
attr[ATTR_BATTERY_LEVEL] = self.vera_device.battery_level + '%' attr[ATTR_BATTERY_LEVEL] = self.vera_device.battery_level + '%'
if self.vera_device.is_armable: if self.vera_device.is_armable:
armed = self.vera_device.refresh_value('Armed') armed = self.vera_device.get_value('Armed')
attr[ATTR_ARMED] = 'True' if armed == '1' else 'False' attr[ATTR_ARMED] = 'True' if armed == '1' else 'False'
if self.vera_device.is_trippable: if self.vera_device.is_trippable:
last_tripped = self.vera_device.refresh_value('LastTrip') last_tripped = self.vera_device.get_value('LastTrip')
if last_tripped is not None: if last_tripped is not None:
utc_time = dt_util.utc_from_timestamp(int(last_tripped)) utc_time = dt_util.utc_from_timestamp(int(last_tripped))
attr[ATTR_LAST_TRIP_TIME] = dt_util.datetime_to_str( attr[ATTR_LAST_TRIP_TIME] = dt_util.datetime_to_str(
utc_time) utc_time)
else: else:
attr[ATTR_LAST_TRIP_TIME] = None attr[ATTR_LAST_TRIP_TIME] = None
tripped = self.vera_device.refresh_value('Tripped') tripped = self.vera_device.get_value('Tripped')
attr[ATTR_TRIPPED] = 'True' if tripped == '1' else 'False' attr[ATTR_TRIPPED] = 'True' if tripped == '1' else 'False'
attr['Vera Device Id'] = self.vera_device.vera_device_id attr['Vera Device Id'] = self.vera_device.vera_device_id
@ -109,25 +132,21 @@ class VeraSwitch(ToggleEntity):
return attr return attr
def turn_on(self, **kwargs): def turn_on(self, **kwargs):
self.last_command_send = time.time()
self.vera_device.switch_on() self.vera_device.switch_on()
self.is_on_status = True self._state = STATE_ON
self.update_ha_state()
def turn_off(self, **kwargs): def turn_off(self, **kwargs):
self.last_command_send = time.time()
self.vera_device.switch_off() self.vera_device.switch_off()
self.is_on_status = False self._state = STATE_OFF
self.update_ha_state()
@property
def should_poll(self):
""" Tells Home Assistant not to poll this entity. """
return False
@property @property
def is_on(self): def is_on(self):
""" True if device is on. """ """ True if device is on. """
return self.is_on_status return self._state == STATE_ON
def update(self):
# We need to debounce the status call after turning switch on or off
# because the vera has some lag in updating the device status
try:
if (self.last_command_send + 5) < time.time():
self.is_on_status = self.vera_device.is_switched_on()
except RequestException:
_LOGGER.warning('Could not update status for %s', self.name)

View file

@ -12,7 +12,7 @@ from homeassistant.components.switch import SwitchDevice
from homeassistant.const import ( from homeassistant.const import (
STATE_ON, STATE_OFF, STATE_STANDBY, EVENT_HOMEASSISTANT_STOP) STATE_ON, STATE_OFF, STATE_STANDBY, EVENT_HOMEASSISTANT_STOP)
REQUIREMENTS = ['pywemo==0.3.7'] REQUIREMENTS = ['pywemo==0.3.8']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
_WEMO_SUBSCRIPTION_REGISTRY = None _WEMO_SUBSCRIPTION_REGISTRY = None
@ -69,15 +69,14 @@ class WemoSwitch(SwitchDevice):
def _update_callback(self, _device, _params): def _update_callback(self, _device, _params):
""" Called by the wemo device callback to update state. """ """ Called by the wemo device callback to update state. """
_LOGGER.info( _LOGGER.info(
'Subscription update for %s, sevice=%s', 'Subscription update for %s',
self.name, _device) _device)
self.update_ha_state(True) self.update_ha_state(True)
@property @property
def should_poll(self): def should_poll(self):
""" No polling should be needed with subscriptions """ """ No polling needed with subscriptions """
# but leave in for initial version in case of issues. return False
return True
@property @property
def unique_id(self): def unique_id(self):

View file

@ -224,12 +224,12 @@ class ThermostatDevice(Entity):
@property @property
def min_temp(self): def min_temp(self):
""" Return minimum temperature. """ """ Return minimum temperature. """
return convert(7, TEMP_CELCIUS, self.unit_of_measurement) return round(convert(7, TEMP_CELCIUS, self.unit_of_measurement))
@property @property
def max_temp(self): def max_temp(self):
""" Return maxmum temperature. """ """ Return maxmum temperature. """
return convert(35, TEMP_CELCIUS, self.unit_of_measurement) return round(convert(35, TEMP_CELCIUS, self.unit_of_measurement))
def _convert(self, temp, round_dec=None): def _convert(self, temp, round_dec=None):
""" Convert temperature from this thermost into user preferred """ Convert temperature from this thermost into user preferred

View file

@ -46,8 +46,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
return return
data = ecobee.NETWORK data = ecobee.NETWORK
hold_temp = discovery_info['hold_temp'] hold_temp = discovery_info['hold_temp']
_LOGGER.info("Loading ecobee thermostat component with hold_temp set to " _LOGGER.info(
+ str(hold_temp)) "Loading ecobee thermostat component with hold_temp set to %s",
hold_temp)
add_devices(Thermostat(data, index, hold_temp) add_devices(Thermostat(data, index, hold_temp)
for index in range(len(data.ecobee.thermostats))) for index in range(len(data.ecobee.thermostats)))

View file

@ -6,8 +6,9 @@ Adds support for Honeywell Round Connected and Honeywell Evohome thermostats.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/thermostat.honeywell/ https://home-assistant.io/components/thermostat.honeywell/
""" """
import socket
import logging import logging
import socket
from homeassistant.components.thermostat import ThermostatDevice from homeassistant.components.thermostat import ThermostatDevice
from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD, TEMP_CELCIUS) from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD, TEMP_CELCIUS)
@ -15,6 +16,8 @@ REQUIREMENTS = ['evohomeclient==0.2.4']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_AWAY_TEMP = "away_temperature"
# pylint: disable=unused-argument # pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
@ -23,17 +26,26 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
username = config.get(CONF_USERNAME) username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD) password = config.get(CONF_PASSWORD)
try:
away_temp = float(config.get(CONF_AWAY_TEMP, 16))
except ValueError:
_LOGGER.error("value entered for item %s should convert to a number",
CONF_AWAY_TEMP)
return False
if username is None or password is None: if username is None or password is None:
_LOGGER.error("Missing required configuration items %s or %s", _LOGGER.error("Missing required configuration items %s or %s",
CONF_USERNAME, CONF_PASSWORD) CONF_USERNAME, CONF_PASSWORD)
return False return False
evo_api = EvohomeClient(username, password) evo_api = EvohomeClient(username, password)
try: try:
zones = evo_api.temperatures(force_refresh=True) zones = evo_api.temperatures(force_refresh=True)
for i, zone in enumerate(zones): for i, zone in enumerate(zones):
add_devices([RoundThermostat(evo_api, zone['id'], i == 0)]) add_devices([RoundThermostat(evo_api,
zone['id'],
i == 0,
away_temp)])
except socket.error: except socket.error:
_LOGGER.error( _LOGGER.error(
"Connection error logging into the honeywell evohome web service" "Connection error logging into the honeywell evohome web service"
@ -44,7 +56,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class RoundThermostat(ThermostatDevice): class RoundThermostat(ThermostatDevice):
""" Represents a Honeywell Round Connected thermostat. """ """ Represents a Honeywell Round Connected thermostat. """
def __init__(self, device, zone_id, master): # pylint: disable=too-many-instance-attributes
def __init__(self, device, zone_id, master, away_temp):
self.device = device self.device = device
self._current_temperature = None self._current_temperature = None
self._target_temperature = None self._target_temperature = None
@ -52,6 +65,8 @@ class RoundThermostat(ThermostatDevice):
self._id = zone_id self._id = zone_id
self._master = master self._master = master
self._is_dhw = False self._is_dhw = False
self._away_temp = away_temp
self._away = False
self.update() self.update()
@property @property
@ -80,6 +95,25 @@ class RoundThermostat(ThermostatDevice):
""" Set new target temperature """ """ Set new target temperature """
self.device.set_temperature(self._name, temperature) self.device.set_temperature(self._name, temperature)
@property
def is_away_mode_on(self):
""" Returns if away mode is on. """
return self._away
def turn_away_mode_on(self):
""" Turns away on.
Evohome does have a proprietary away mode, but it doesn't really work
the way it should. For example: If you set a temperature manually
it doesn't get overwritten when away mode is switched on.
"""
self._away = True
self.device.set_temperature(self._name, self._away_temp)
def turn_away_mode_off(self):
""" Turns away off. """
self._away = False
self.device.cancel_temp_override(self._name)
def update(self): def update(self):
try: try:
# Only refresh if this is the "master" device, # Only refresh if this is the "master" device,

View file

@ -28,7 +28,7 @@ DISCOVER_SWITCHES = 'verisure.switches'
DISCOVER_ALARMS = 'verisure.alarm_control_panel' DISCOVER_ALARMS = 'verisure.alarm_control_panel'
DEPENDENCIES = ['alarm_control_panel'] DEPENDENCIES = ['alarm_control_panel']
REQUIREMENTS = ['vsure==0.4.3'] REQUIREMENTS = ['vsure==0.4.5']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)

View file

@ -26,6 +26,9 @@ CONF_POLLING_INTERVAL = "polling_interval"
DEFAULT_ZWAVE_CONFIG_PATH = os.path.join(sys.prefix, 'share', DEFAULT_ZWAVE_CONFIG_PATH = os.path.join(sys.prefix, 'share',
'python-openzwave', 'config') 'python-openzwave', 'config')
SERVICE_ADD_NODE = "add_node"
SERVICE_REMOVE_NODE = "remove_node"
DISCOVER_SENSORS = "zwave.sensors" DISCOVER_SENSORS = "zwave.sensors"
DISCOVER_SWITCHES = "zwave.switch" DISCOVER_SWITCHES = "zwave.switch"
DISCOVER_LIGHTS = "zwave.light" DISCOVER_LIGHTS = "zwave.light"
@ -37,6 +40,7 @@ COMMAND_CLASS_SENSOR_BINARY = 48
COMMAND_CLASS_SENSOR_MULTILEVEL = 49 COMMAND_CLASS_SENSOR_MULTILEVEL = 49
COMMAND_CLASS_METER = 50 COMMAND_CLASS_METER = 50
COMMAND_CLASS_BATTERY = 128 COMMAND_CLASS_BATTERY = 128
COMMAND_CLASS_ALARM = 113 # 0x71
GENRE_WHATEVER = None GENRE_WHATEVER = None
GENRE_USER = "User" GENRE_USER = "User"
@ -53,7 +57,8 @@ DISCOVERY_COMPONENTS = [
DISCOVER_SENSORS, DISCOVER_SENSORS,
[COMMAND_CLASS_SENSOR_BINARY, [COMMAND_CLASS_SENSOR_BINARY,
COMMAND_CLASS_SENSOR_MULTILEVEL, COMMAND_CLASS_SENSOR_MULTILEVEL,
COMMAND_CLASS_METER], COMMAND_CLASS_METER,
COMMAND_CLASS_ALARM],
TYPE_WHATEVER, TYPE_WHATEVER,
GENRE_USER), GENRE_USER),
('light', ('light',
@ -176,6 +181,14 @@ def setup(hass, config):
dispatcher.connect( dispatcher.connect(
value_added, ZWaveNetwork.SIGNAL_VALUE_ADDED, weak=False) value_added, ZWaveNetwork.SIGNAL_VALUE_ADDED, weak=False)
def add_node(event):
""" Switch into inclusion mode """
NETWORK.controller.begin_command_add_device()
def remove_node(event):
""" Switch into exclusion mode"""
NETWORK.controller.begin_command_remove_device()
def stop_zwave(event): def stop_zwave(event):
""" Stop Z-wave. """ """ Stop Z-wave. """
NETWORK.stop() NETWORK.stop()
@ -190,6 +203,11 @@ def setup(hass, config):
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_zwave) hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_zwave)
# register add / remove node services for zwave sticks without
# hardware inclusion button
hass.services.register(DOMAIN, SERVICE_ADD_NODE, add_node)
hass.services.register(DOMAIN, SERVICE_REMOVE_NODE, remove_node)
hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_zwave) hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_zwave)
return True return True

View file

@ -24,6 +24,7 @@ CONF_USERNAME = "username"
CONF_PASSWORD = "password" CONF_PASSWORD = "password"
CONF_API_KEY = "api_key" CONF_API_KEY = "api_key"
CONF_ACCESS_TOKEN = "access_token" CONF_ACCESS_TOKEN = "access_token"
CONF_FILENAME = "filename"
CONF_VALUE_TEMPLATE = "value_template" CONF_VALUE_TEMPLATE = "value_template"

View file

@ -36,7 +36,7 @@ def extract_entity_ids(hass, service):
service_ent_id = service.data[ATTR_ENTITY_ID] service_ent_id = service.data[ATTR_ENTITY_ID]
if isinstance(service_ent_id, str): if isinstance(service_ent_id, str):
return group.expand_entity_ids(hass, [service_ent_id.lower()]) return group.expand_entity_ids(hass, [service_ent_id])
return [ent_id for ent_id in group.expand_entity_ids(hass, service_ent_id)] return [ent_id for ent_id in group.expand_entity_ids(hass, service_ent_id)]

View file

@ -0,0 +1,43 @@
"""Service calling related helpers."""
import logging
from homeassistant.util import split_entity_id
from homeassistant.const import ATTR_ENTITY_ID
CONF_SERVICE = 'service'
CONF_SERVICE_ENTITY_ID = 'entity_id'
CONF_SERVICE_DATA = 'data'
_LOGGER = logging.getLogger(__name__)
def call_from_config(hass, config, blocking=False):
"""Call a service based on a config hash."""
if not isinstance(config, dict) or CONF_SERVICE not in config:
_LOGGER.error('Missing key %s: %s', CONF_SERVICE, config)
return
try:
domain, service = split_entity_id(config[CONF_SERVICE])
except ValueError:
_LOGGER.error('Invalid service specified: %s', config[CONF_SERVICE])
return
service_data = config.get(CONF_SERVICE_DATA)
if service_data is None:
service_data = {}
elif isinstance(service_data, dict):
service_data = dict(service_data)
else:
_LOGGER.error("%s should be a dictionary", CONF_SERVICE_DATA)
service_data = {}
entity_id = config.get(CONF_SERVICE_ENTITY_ID)
if isinstance(entity_id, str):
service_data[ATTR_ENTITY_ID] = [ent.strip() for ent in
entity_id.split(",")]
elif entity_id is not None:
service_data[ATTR_ENTITY_ID] = entity_id
hass.services.call(domain, service, service_data, blocking)

View file

@ -1,9 +1,6 @@
""" """Helpers that help with state related things."""
homeassistant.helpers.state from collections import defaultdict
~~~~~~~~~~~~~~~~~~~~~~~~~~~ import json
Helpers that help with state related things.
"""
import logging import logging
from homeassistant.core import State from homeassistant.core import State
@ -25,32 +22,36 @@ class TrackStates(object):
that have changed since the start time to the return list when with-block that have changed since the start time to the return list when with-block
is exited. is exited.
""" """
def __init__(self, hass): def __init__(self, hass):
"""Initialize a TrackStates block."""
self.hass = hass self.hass = hass
self.states = [] self.states = []
def __enter__(self): def __enter__(self):
"""Record time from which to track changes."""
self.now = dt_util.utcnow() self.now = dt_util.utcnow()
return self.states return self.states
def __exit__(self, exc_type, exc_value, traceback): def __exit__(self, exc_type, exc_value, traceback):
"""Add changes states to changes list."""
self.states.extend(get_changed_since(self.hass.states.all(), self.now)) self.states.extend(get_changed_since(self.hass.states.all(), self.now))
def get_changed_since(states, utc_point_in_time): def get_changed_since(states, utc_point_in_time):
""" """List of states that have been changed since utc_point_in_time."""
Returns all states that have been changed since utc_point_in_time.
"""
point_in_time = dt_util.strip_microseconds(utc_point_in_time) point_in_time = dt_util.strip_microseconds(utc_point_in_time)
return [state for state in states if state.last_updated >= point_in_time] return [state for state in states if state.last_updated >= point_in_time]
def reproduce_state(hass, states, blocking=False): def reproduce_state(hass, states, blocking=False):
""" Takes in a state and will try to have the entity reproduce it. """ """Reproduce given state."""
if isinstance(states, State): if isinstance(states, State):
states = [states] states = [states]
to_call = defaultdict(list)
for state in states: for state in states:
current_state = hass.states.get(state.entity_id) current_state = hass.states.get(state.entity_id)
@ -76,7 +77,18 @@ def reproduce_state(hass, states, blocking=False):
state) state)
continue continue
service_data = dict(state.attributes) if state.domain == 'group':
service_data[ATTR_ENTITY_ID] = state.entity_id service_domain = 'homeassistant'
else:
service_domain = state.domain
hass.services.call(state.domain, service, service_data, blocking) # We group service calls for entities by service call
# json used to create a hashable version of dict with maybe lists in it
key = (service_domain, service,
json.dumps(state.attributes, sort_keys=True))
to_call[key].append(state.entity_id)
for (service_domain, service, service_data), entity_ids in to_call.items():
data = json.loads(service_data)
data[ATTR_ENTITY_ID] = entity_ids
hass.services.call(service_domain, service, data, blocking)

View file

@ -16,7 +16,7 @@ fuzzywuzzy==0.8.0
pyicloud==0.7.2 pyicloud==0.7.2
# homeassistant.components.device_tracker.netgear # homeassistant.components.device_tracker.netgear
pynetgear==0.3 pynetgear==0.3.1
# homeassistant.components.device_tracker.nmap_tracker # homeassistant.components.device_tracker.nmap_tracker
python-nmap==0.4.3 python-nmap==0.4.3
@ -59,7 +59,7 @@ tellcore-py==1.1.2
# homeassistant.components.light.vera # homeassistant.components.light.vera
# homeassistant.components.sensor.vera # homeassistant.components.sensor.vera
# homeassistant.components.switch.vera # homeassistant.components.switch.vera
https://github.com/pavoni/home-assistant-vera-api/archive/efdba4e63d58a30bc9b36d9e01e69858af9130b8.zip#python-vera==0.1.1 pyvera==0.2.3
# homeassistant.components.wink # homeassistant.components.wink
# homeassistant.components.light.wink # homeassistant.components.light.wink
@ -69,7 +69,7 @@ https://github.com/pavoni/home-assistant-vera-api/archive/efdba4e63d58a30bc9b36d
python-wink==0.3.1 python-wink==0.3.1
# homeassistant.components.media_player.cast # homeassistant.components.media_player.cast
pychromecast==0.6.13 pychromecast==0.6.14
# homeassistant.components.media_player.kodi # homeassistant.components.media_player.kodi
jsonrpc-requests==0.1 jsonrpc-requests==0.1
@ -92,6 +92,12 @@ paho-mqtt==1.1
# homeassistant.components.nest # homeassistant.components.nest
python-nest==2.6.0 python-nest==2.6.0
# homeassistant.components.mysensors
https://github.com/theolind/pymysensors/archive/005bff4c5ca7a56acd30e816bc3bcdb5cb2d46fd.zip#pymysensors==0.4
# homeassistant.components.notify.free_mobile
freesms==0.1.0
# homeassistant.components.notify.pushbullet # homeassistant.components.notify.pushbullet
pushbullet.py==0.9.0 pushbullet.py==0.9.0
@ -134,11 +140,11 @@ eliqonline==1.0.11
# homeassistant.components.sensor.forecast # homeassistant.components.sensor.forecast
python-forecastio==1.3.3 python-forecastio==1.3.3
# homeassistant.components.sensor.mysensors # homeassistant.components.sensor.netatmo
https://github.com/theolind/pymysensors/archive/d4b809c2167650691058d1e29bfd2c4b1792b4b0.zip#pymysensors==0.3 https://github.com/HydrelioxGitHub/netatmo-api-python/archive/43ff238a0122b0939a0dc4e8836b6782913fb6e2.zip#lnetatmo==0.4.0
# homeassistant.components.sensor.openweathermap # homeassistant.components.sensor.openweathermap
pyowm==2.2.1 pyowm==2.3.0
# homeassistant.components.sensor.rpi_gpio # homeassistant.components.sensor.rpi_gpio
# homeassistant.components.switch.rpi_gpio # homeassistant.components.switch.rpi_gpio
@ -176,7 +182,7 @@ hikvision==0.4
orvibo==1.1.0 orvibo==1.1.0
# homeassistant.components.switch.wemo # homeassistant.components.switch.wemo
pywemo==0.3.7 pywemo==0.3.8
# homeassistant.components.tellduslive # homeassistant.components.tellduslive
tellive-py==0.5.2 tellive-py==0.5.2
@ -191,7 +197,7 @@ evohomeclient==0.2.4
radiotherm==1.2 radiotherm==1.2
# homeassistant.components.verisure # homeassistant.components.verisure
vsure==0.4.3 vsure==0.4.5
# homeassistant.components.zwave # homeassistant.components.zwave
pydispatcher==2.0.5 pydispatcher==2.0.5

View file

@ -1,5 +1,5 @@
flake8>=2.5.0 flake8>=2.5.1
pylint>=1.5.1 pylint>=1.5.3
coveralls>=1.1 coveralls>=1.1
pytest>=2.6.4 pytest>=2.6.4
pytest-cov>=2.2.0 pytest-cov>=2.2.0

View file

@ -5,6 +5,28 @@
cd "$(dirname "$0")/.." cd "$(dirname "$0")/.."
if [ "$TRAVIS_PYTHON_VERSION" = "3.5" ]; then
echo "Verifying requirements_all.txt..."
python3 setup.py -q develop 2> /dev/null
tput setaf 1
script/gen_requirements_all.py validate
VERIFY_REQUIREMENTS_STATUS=$?
tput sgr0
else
VERIFY_REQUIREMENTS_STATUS=0
fi
if [ "$VERIFY_REQUIREMENTS_STATUS" != "0" ]; then
exit $VERIFY_REQUIREMENTS_STATUS
fi
script/bootstrap_server > /dev/null
DEP_INSTALL_STATUS=$?
if [ "$DEP_INSTALL_STATUS" != "0" ]; then
exit $DEP_INSTALL_STATUS
fi
if [ "$TRAVIS_PYTHON_VERSION" != "3.5" ]; then if [ "$TRAVIS_PYTHON_VERSION" != "3.5" ]; then
NO_LINT=1 NO_LINT=1
fi fi

View file

@ -5,13 +5,6 @@
cd "$(dirname "$0")/.." cd "$(dirname "$0")/.."
if [ "$NO_LINT" = "1" ]; then
LINT_STATUS=0
else
script/lint
LINT_STATUS=$?
fi
echo "Running tests..." echo "Running tests..."
if [ "$1" = "coverage" ]; then if [ "$1" = "coverage" ]; then
@ -22,6 +15,13 @@ else
TEST_STATUS=$? TEST_STATUS=$?
fi fi
if [ "$NO_LINT" = "1" ]; then
LINT_STATUS=0
else
script/lint
LINT_STATUS=$?
fi
if [ $LINT_STATUS -eq 0 ] if [ $LINT_STATUS -eq 0 ]
then then
exit $TEST_STATUS exit $TEST_STATUS

View file

@ -162,14 +162,14 @@ class TestAutomationSun(unittest.TestCase):
}) })
now = datetime(2015, 9, 16, 15, tzinfo=dt_util.UTC) now = datetime(2015, 9, 16, 15, tzinfo=dt_util.UTC)
with patch('homeassistant.components.automation.sun.dt_util.utcnow', with patch('homeassistant.components.automation.sun.dt_util.now',
return_value=now): return_value=now):
self.hass.bus.fire('test_event') self.hass.bus.fire('test_event')
self.hass.pool.block_till_done() self.hass.pool.block_till_done()
self.assertEqual(0, len(self.calls)) self.assertEqual(0, len(self.calls))
now = datetime(2015, 9, 16, 10, tzinfo=dt_util.UTC) now = datetime(2015, 9, 16, 10, tzinfo=dt_util.UTC)
with patch('homeassistant.components.automation.sun.dt_util.utcnow', with patch('homeassistant.components.automation.sun.dt_util.now',
return_value=now): return_value=now):
self.hass.bus.fire('test_event') self.hass.bus.fire('test_event')
self.hass.pool.block_till_done() self.hass.pool.block_till_done()
@ -197,14 +197,14 @@ class TestAutomationSun(unittest.TestCase):
}) })
now = datetime(2015, 9, 16, 13, tzinfo=dt_util.UTC) now = datetime(2015, 9, 16, 13, tzinfo=dt_util.UTC)
with patch('homeassistant.components.automation.sun.dt_util.utcnow', with patch('homeassistant.components.automation.sun.dt_util.now',
return_value=now): return_value=now):
self.hass.bus.fire('test_event') self.hass.bus.fire('test_event')
self.hass.pool.block_till_done() self.hass.pool.block_till_done()
self.assertEqual(0, len(self.calls)) self.assertEqual(0, len(self.calls))
now = datetime(2015, 9, 16, 15, tzinfo=dt_util.UTC) now = datetime(2015, 9, 16, 15, tzinfo=dt_util.UTC)
with patch('homeassistant.components.automation.sun.dt_util.utcnow', with patch('homeassistant.components.automation.sun.dt_util.now',
return_value=now): return_value=now):
self.hass.bus.fire('test_event') self.hass.bus.fire('test_event')
self.hass.pool.block_till_done() self.hass.pool.block_till_done()
@ -233,14 +233,14 @@ class TestAutomationSun(unittest.TestCase):
}) })
now = datetime(2015, 9, 16, 15, 1, tzinfo=dt_util.UTC) now = datetime(2015, 9, 16, 15, 1, tzinfo=dt_util.UTC)
with patch('homeassistant.components.automation.sun.dt_util.utcnow', with patch('homeassistant.components.automation.sun.dt_util.now',
return_value=now): return_value=now):
self.hass.bus.fire('test_event') self.hass.bus.fire('test_event')
self.hass.pool.block_till_done() self.hass.pool.block_till_done()
self.assertEqual(0, len(self.calls)) self.assertEqual(0, len(self.calls))
now = datetime(2015, 9, 16, 15, tzinfo=dt_util.UTC) now = datetime(2015, 9, 16, 15, tzinfo=dt_util.UTC)
with patch('homeassistant.components.automation.sun.dt_util.utcnow', with patch('homeassistant.components.automation.sun.dt_util.now',
return_value=now): return_value=now):
self.hass.bus.fire('test_event') self.hass.bus.fire('test_event')
self.hass.pool.block_till_done() self.hass.pool.block_till_done()
@ -269,14 +269,14 @@ class TestAutomationSun(unittest.TestCase):
}) })
now = datetime(2015, 9, 16, 14, 59, tzinfo=dt_util.UTC) now = datetime(2015, 9, 16, 14, 59, tzinfo=dt_util.UTC)
with patch('homeassistant.components.automation.sun.dt_util.utcnow', with patch('homeassistant.components.automation.sun.dt_util.now',
return_value=now): return_value=now):
self.hass.bus.fire('test_event') self.hass.bus.fire('test_event')
self.hass.pool.block_till_done() self.hass.pool.block_till_done()
self.assertEqual(0, len(self.calls)) self.assertEqual(0, len(self.calls))
now = datetime(2015, 9, 16, 15, tzinfo=dt_util.UTC) now = datetime(2015, 9, 16, 15, tzinfo=dt_util.UTC)
with patch('homeassistant.components.automation.sun.dt_util.utcnow', with patch('homeassistant.components.automation.sun.dt_util.now',
return_value=now): return_value=now):
self.hass.bus.fire('test_event') self.hass.bus.fire('test_event')
self.hass.pool.block_till_done() self.hass.pool.block_till_done()
@ -306,21 +306,60 @@ class TestAutomationSun(unittest.TestCase):
}) })
now = datetime(2015, 9, 16, 9, 59, tzinfo=dt_util.UTC) now = datetime(2015, 9, 16, 9, 59, tzinfo=dt_util.UTC)
with patch('homeassistant.components.automation.sun.dt_util.utcnow', with patch('homeassistant.components.automation.sun.dt_util.now',
return_value=now): return_value=now):
self.hass.bus.fire('test_event') self.hass.bus.fire('test_event')
self.hass.pool.block_till_done() self.hass.pool.block_till_done()
self.assertEqual(0, len(self.calls)) self.assertEqual(0, len(self.calls))
now = datetime(2015, 9, 16, 15, 1, tzinfo=dt_util.UTC) now = datetime(2015, 9, 16, 15, 1, tzinfo=dt_util.UTC)
with patch('homeassistant.components.automation.sun.dt_util.utcnow', with patch('homeassistant.components.automation.sun.dt_util.now',
return_value=now): return_value=now):
self.hass.bus.fire('test_event') self.hass.bus.fire('test_event')
self.hass.pool.block_till_done() self.hass.pool.block_till_done()
self.assertEqual(0, len(self.calls)) self.assertEqual(0, len(self.calls))
now = datetime(2015, 9, 16, 12, tzinfo=dt_util.UTC) now = datetime(2015, 9, 16, 12, tzinfo=dt_util.UTC)
with patch('homeassistant.components.automation.sun.dt_util.utcnow', with patch('homeassistant.components.automation.sun.dt_util.now',
return_value=now):
self.hass.bus.fire('test_event')
self.hass.pool.block_till_done()
self.assertEqual(1, len(self.calls))
def test_if_action_after_different_tz(self):
import pytz
self.hass.states.set(sun.ENTITY_ID, sun.STATE_ABOVE_HORIZON, {
sun.STATE_ATTR_NEXT_SETTING: '17:30:00 16-09-2015',
})
automation.setup(self.hass, {
automation.DOMAIN: {
'trigger': {
'platform': 'event',
'event_type': 'test_event',
},
'condition': {
'platform': 'sun',
'after': 'sunset',
},
'action': {
'service': 'test.automation'
}
}
})
# Before
now = datetime(2015, 9, 16, 17, tzinfo=pytz.timezone('US/Mountain'))
with patch('homeassistant.components.automation.sun.dt_util.now',
return_value=now):
self.hass.bus.fire('test_event')
self.hass.pool.block_till_done()
self.assertEqual(0, len(self.calls))
# After
now = datetime(2015, 9, 16, 18, tzinfo=pytz.timezone('US/Mountain'))
with patch('homeassistant.components.automation.sun.dt_util.now',
return_value=now): return_value=now):
self.hass.bus.fire('test_event') self.hass.bus.fire('test_event')
self.hass.pool.block_till_done() self.hass.pool.block_till_done()

View file

@ -11,10 +11,10 @@ from unittest.mock import patch
import requests import requests
from homeassistant import bootstrap, const from homeassistant import bootstrap, const
import homeassistant.core as ha
import homeassistant.components.device_tracker as device_tracker import homeassistant.components.device_tracker as device_tracker
import homeassistant.components.http as http import homeassistant.components.http as http
import homeassistant.components.zone as zone
from tests.common import get_test_home_assistant
SERVER_PORT = 8126 SERVER_PORT = 8126
HTTP_BASE_URL = "http://127.0.0.1:{}".format(SERVER_PORT) HTTP_BASE_URL = "http://127.0.0.1:{}".format(SERVER_PORT)
@ -34,7 +34,7 @@ def setUpModule(mock_get_local_ip): # pylint: disable=invalid-name
""" Initalizes a Home Assistant server. """ """ Initalizes a Home Assistant server. """
global hass global hass
hass = ha.HomeAssistant() hass = get_test_home_assistant()
# Set up server # Set up server
bootstrap.setup_component(hass, http.DOMAIN, { bootstrap.setup_component(hass, http.DOMAIN, {

View file

@ -0,0 +1,342 @@
"""
tests.component.media_player.test_universal
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Tests universal media_player component.
"""
from copy import copy
import unittest
import homeassistant.core as ha
from homeassistant.const import (
STATE_OFF, STATE_ON, STATE_UNKNOWN, STATE_PLAYING, STATE_PAUSED)
import homeassistant.components.switch as switch
import homeassistant.components.media_player as media_player
import homeassistant.components.media_player.universal as universal
class MockMediaPlayer(media_player.MediaPlayerDevice):
""" Mock media player for testing """
def __init__(self, hass, name):
self.hass = hass
self._name = name
self.entity_id = media_player.ENTITY_ID_FORMAT.format(name)
self._state = STATE_OFF
self._volume_level = 0
self._is_volume_muted = False
self._media_title = None
self._supported_media_commands = 0
@property
def name(self):
""" name of player """
return self._name
@property
def state(self):
""" state of the player """
return self._state
@property
def volume_level(self):
""" volume level of player """
return self._volume_level
@property
def is_volume_muted(self):
""" if the media player is muted """
return self._is_volume_muted
@property
def supported_media_commands(self):
""" supported media commands flag """
return self._supported_media_commands
def turn_on(self):
""" mock turn_on function """
self._state = STATE_UNKNOWN
def turn_off(self):
""" mock turn_off function """
self._state = STATE_OFF
def mute_volume(self):
""" mock mute function """
self._is_volume_muted = ~self._is_volume_muted
def set_volume_level(self, volume):
""" mock set volume level """
self._volume_level = volume
def media_play(self):
""" mock play """
self._state = STATE_PLAYING
def media_pause(self):
""" mock pause """
self._state = STATE_PAUSED
class TestMediaPlayer(unittest.TestCase):
""" Test the media_player module. """
def setUp(self): # pylint: disable=invalid-name
self.hass = ha.HomeAssistant()
self.mock_mp_1 = MockMediaPlayer(self.hass, 'mock1')
self.mock_mp_1.update_ha_state()
self.mock_mp_2 = MockMediaPlayer(self.hass, 'mock2')
self.mock_mp_2.update_ha_state()
self.mock_mute_switch_id = switch.ENTITY_ID_FORMAT.format('mute')
self.hass.states.set(self.mock_mute_switch_id, STATE_OFF)
self.mock_state_switch_id = switch.ENTITY_ID_FORMAT.format('state')
self.hass.states.set(self.mock_state_switch_id, STATE_OFF)
self.config_children_only = \
{'name': 'test', 'platform': 'universal',
'children': [media_player.ENTITY_ID_FORMAT.format('mock1'),
media_player.ENTITY_ID_FORMAT.format('mock2')]}
self.config_children_and_attr = \
{'name': 'test', 'platform': 'universal',
'children': [media_player.ENTITY_ID_FORMAT.format('mock1'),
media_player.ENTITY_ID_FORMAT.format('mock2')],
'attributes': {
'is_volume_muted': self.mock_mute_switch_id,
'state': self.mock_state_switch_id}}
def tearDown(self): # pylint: disable=invalid-name
""" Stop down stuff we started. """
self.hass.stop()
def test_check_config_children_only(self):
""" Check config with only children """
config_start = copy(self.config_children_only)
del config_start['platform']
config_start['commands'] = {}
config_start['attributes'] = {}
response = universal.validate_config(self.config_children_only)
self.assertTrue(response)
self.assertEqual(config_start, self.config_children_only)
def test_check_config_children_and_attr(self):
""" Check config with children and attributes """
config_start = copy(self.config_children_and_attr)
del config_start['platform']
config_start['commands'] = {}
response = universal.validate_config(self.config_children_and_attr)
self.assertTrue(response)
self.assertEqual(config_start, self.config_children_and_attr)
def test_master_state(self):
""" test master state property """
config = self.config_children_only
universal.validate_config(config)
ump = universal.UniversalMediaPlayer(self.hass, **config)
self.assertEqual(None, ump.master_state)
def test_master_state_with_attrs(self):
""" test master state property """
config = self.config_children_and_attr
universal.validate_config(config)
ump = universal.UniversalMediaPlayer(self.hass, **config)
self.assertEqual(STATE_OFF, ump.master_state)
self.hass.states.set(self.mock_state_switch_id, STATE_ON)
self.assertEqual(STATE_ON, ump.master_state)
def test_master_state_with_bad_attrs(self):
""" test master state property """
config = self.config_children_and_attr
config['attributes']['state'] = 'bad.entity_id'
universal.validate_config(config)
ump = universal.UniversalMediaPlayer(self.hass, **config)
self.assertEqual(STATE_OFF, ump.master_state)
def test_active_child_state(self):
""" test active child state property """
config = self.config_children_only
universal.validate_config(config)
ump = universal.UniversalMediaPlayer(self.hass, **config)
ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config['name'])
ump.update()
self.assertEqual(None, ump._child_state)
self.mock_mp_1._state = STATE_PLAYING
self.mock_mp_1.update_ha_state()
ump.update()
self.assertEqual(self.mock_mp_1.entity_id,
ump._child_state.entity_id)
self.mock_mp_2._state = STATE_PLAYING
self.mock_mp_2.update_ha_state()
ump.update()
self.assertEqual(self.mock_mp_1.entity_id,
ump._child_state.entity_id)
self.mock_mp_1._state = STATE_OFF
self.mock_mp_1.update_ha_state()
ump.update()
self.assertEqual(self.mock_mp_2.entity_id,
ump._child_state.entity_id)
def test_name(self):
""" test name property """
config = self.config_children_only
universal.validate_config(config)
ump = universal.UniversalMediaPlayer(self.hass, **config)
self.assertEqual(config['name'], ump.name)
def test_state_children_only(self):
""" test media player state with only children """
config = self.config_children_only
universal.validate_config(config)
ump = universal.UniversalMediaPlayer(self.hass, **config)
ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config['name'])
ump.update()
self.assertTrue(ump.state, STATE_OFF)
self.mock_mp_1._state = STATE_PLAYING
self.mock_mp_1.update_ha_state()
ump.update()
self.assertEqual(STATE_PLAYING, ump.state)
def test_state_with_children_and_attrs(self):
""" test media player with children and master state """
config = self.config_children_and_attr
universal.validate_config(config)
ump = universal.UniversalMediaPlayer(self.hass, **config)
ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config['name'])
ump.update()
self.assertEqual(ump.state, STATE_OFF)
self.hass.states.set(self.mock_state_switch_id, STATE_ON)
ump.update()
self.assertEqual(ump.state, STATE_ON)
self.mock_mp_1._state = STATE_PLAYING
self.mock_mp_1.update_ha_state()
ump.update()
self.assertEqual(ump.state, STATE_PLAYING)
self.hass.states.set(self.mock_state_switch_id, STATE_OFF)
ump.update()
self.assertEqual(ump.state, STATE_OFF)
def test_volume_level(self):
""" test volume level property """
config = self.config_children_only
universal.validate_config(config)
ump = universal.UniversalMediaPlayer(self.hass, **config)
ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config['name'])
ump.update()
self.assertEqual(None, ump.volume_level)
self.mock_mp_1._state = STATE_PLAYING
self.mock_mp_1.update_ha_state()
ump.update()
self.assertEqual(0, ump.volume_level)
self.mock_mp_1._volume_level = 1
self.mock_mp_1.update_ha_state()
ump.update()
self.assertEqual(1, ump.volume_level)
def test_is_volume_muted_children_only(self):
""" test is volume muted property w/ children only """
config = self.config_children_only
universal.validate_config(config)
ump = universal.UniversalMediaPlayer(self.hass, **config)
ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config['name'])
ump.update()
self.assertFalse(ump.is_volume_muted)
self.mock_mp_1._state = STATE_PLAYING
self.mock_mp_1.update_ha_state()
ump.update()
self.assertFalse(ump.is_volume_muted)
self.mock_mp_1._is_volume_muted = True
self.mock_mp_1.update_ha_state()
ump.update()
self.assertTrue(ump.is_volume_muted)
def test_is_volume_muted_children_and_attr(self):
""" test is volume muted property w/ children and attrs """
config = self.config_children_and_attr
universal.validate_config(config)
ump = universal.UniversalMediaPlayer(self.hass, **config)
self.assertFalse(ump.is_volume_muted)
self.hass.states.set(self.mock_mute_switch_id, STATE_ON)
self.assertTrue(ump.is_volume_muted)
def test_supported_media_commands_children_only(self):
""" test supported media commands with only children """
config = self.config_children_only
universal.validate_config(config)
ump = universal.UniversalMediaPlayer(self.hass, **config)
ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config['name'])
ump.update()
self.assertEqual(0, ump.supported_media_commands)
self.mock_mp_1._supported_media_commands = 512
self.mock_mp_1._state = STATE_PLAYING
self.mock_mp_1.update_ha_state()
ump.update()
self.assertEqual(512, ump.supported_media_commands)
def test_supported_media_commands_children_and_cmds(self):
""" test supported media commands with children and attrs """
config = self.config_children_and_attr
universal.validate_config(config)
config['commands']['turn_on'] = 'test'
config['commands']['turn_off'] = 'test'
config['commands']['volume_up'] = 'test'
config['commands']['volume_down'] = 'test'
config['commands']['volume_mute'] = 'test'
ump = universal.UniversalMediaPlayer(self.hass, **config)
ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config['name'])
ump.update()
self.mock_mp_1._supported_media_commands = \
universal.SUPPORT_VOLUME_SET
self.mock_mp_1._state = STATE_PLAYING
self.mock_mp_1.update_ha_state()
ump.update()
check_flags = universal.SUPPORT_TURN_ON | universal.SUPPORT_TURN_OFF \
| universal.SUPPORT_VOLUME_STEP | universal.SUPPORT_VOLUME_MUTE
self.assertEqual(check_flags, ump.supported_media_commands)

View file

@ -4,12 +4,14 @@ tests.components.sensor.test_yr
Tests Yr sensor. Tests Yr sensor.
""" """
from datetime import datetime
from unittest.mock import patch from unittest.mock import patch
import pytest import pytest
import homeassistant.core as ha import homeassistant.core as ha
import homeassistant.components.sensor as sensor import homeassistant.components.sensor as sensor
import homeassistant.util.dt as dt_util
@pytest.mark.usefixtures('betamax_session') @pytest.mark.usefixtures('betamax_session')
@ -26,14 +28,18 @@ class TestSensorYr:
self.hass.stop() self.hass.stop()
def test_default_setup(self, betamax_session): def test_default_setup(self, betamax_session):
now = datetime(2016, 1, 5, 1, tzinfo=dt_util.UTC)
with patch('homeassistant.components.sensor.yr.requests.Session', with patch('homeassistant.components.sensor.yr.requests.Session',
return_value=betamax_session): return_value=betamax_session):
assert sensor.setup(self.hass, { with patch('homeassistant.components.sensor.yr.dt_util.utcnow',
'sensor': { return_value=now):
'platform': 'yr', assert sensor.setup(self.hass, {
'elevation': 0, 'sensor': {
} 'platform': 'yr',
}) 'elevation': 0,
}
})
state = self.hass.states.get('sensor.yr_symbol') state = self.hass.states.get('sensor.yr_symbol')

View file

@ -27,12 +27,13 @@ API_URL = "http://127.0.0.1:{}{}".format(SERVER_PORT, alexa.API_ENDPOINT)
HA_HEADERS = {const.HTTP_HEADER_HA_AUTH: API_PASSWORD} HA_HEADERS = {const.HTTP_HEADER_HA_AUTH: API_PASSWORD}
hass = None hass = None
calls = []
@patch('homeassistant.components.http.util.get_local_ip', @patch('homeassistant.components.http.util.get_local_ip',
return_value='127.0.0.1') return_value='127.0.0.1')
def setUpModule(mock_get_local_ip): # pylint: disable=invalid-name def setUpModule(mock_get_local_ip): # pylint: disable=invalid-name
""" Initalizes a Home Assistant server. """ """Initalize a Home Assistant server for testing this module."""
global hass global hass
hass = ha.HomeAssistant() hass = ha.HomeAssistant()
@ -42,6 +43,8 @@ def setUpModule(mock_get_local_ip): # pylint: disable=invalid-name
{http.DOMAIN: {http.CONF_API_PASSWORD: API_PASSWORD, {http.DOMAIN: {http.CONF_API_PASSWORD: API_PASSWORD,
http.CONF_SERVER_PORT: SERVER_PORT}}) http.CONF_SERVER_PORT: SERVER_PORT}})
hass.services.register('test', 'alexa', lambda call: calls.append(call))
bootstrap.setup_component(hass, alexa.DOMAIN, { bootstrap.setup_component(hass, alexa.DOMAIN, {
'alexa': { 'alexa': {
'intents': { 'intents': {
@ -61,7 +64,20 @@ def setUpModule(mock_get_local_ip): # pylint: disable=invalid-name
'GetZodiacHoroscopeIntent': { 'GetZodiacHoroscopeIntent': {
'speech': { 'speech': {
'type': 'plaintext', 'type': 'plaintext',
'text': 'You told us your sign is {{ ZodiacSign }}.' 'text': 'You told us your sign is {{ ZodiacSign }}.',
}
},
'CallServiceIntent': {
'speech': {
'type': 'plaintext',
'text': 'Service called',
},
'action': {
'service': 'test.alexa',
'data': {
'hello': 1
},
'entity_id': 'switch.test',
} }
} }
} }
@ -231,6 +247,39 @@ class TestAlexa(unittest.TestCase):
text = req.json().get('response', {}).get('outputSpeech', {}).get('text') text = req.json().get('response', {}).get('outputSpeech', {}).get('text')
self.assertEqual('You are both home, you silly', text) self.assertEqual('You are both home, you silly', text)
def test_intent_request_calling_service(self):
data = {
'version': '1.0',
'session': {
'new': False,
'sessionId': 'amzn1.echo-api.session.0000000-0000-0000-0000-00000000000',
'application': {
'applicationId': 'amzn1.echo-sdk-ams.app.000000-d0ed-0000-ad00-000000d00ebe'
},
'attributes': {},
'user': {
'userId': 'amzn1.account.AM3B00000000000000000000000'
}
},
'request': {
'type': 'IntentRequest',
'requestId': ' amzn1.echo-api.request.0000000-0000-0000-0000-00000000000',
'timestamp': '2015-05-13T12:34:56Z',
'intent': {
'name': 'CallServiceIntent',
}
}
}
call_count = len(calls)
req = _req(data)
self.assertEqual(200, req.status_code)
self.assertEqual(call_count + 1, len(calls))
call = calls[-1]
self.assertEqual('test', call.domain)
self.assertEqual('alexa', call.service)
self.assertEqual(['switch.test'], call.data.get('entity_id'))
self.assertEqual(1, call.data.get('hello'))
def test_session_ended_request(self): def test_session_ended_request(self):
data = { data = {
'version': '1.0', 'version': '1.0',

View file

@ -6,20 +6,22 @@ Tests core compoments.
""" """
# pylint: disable=protected-access,too-many-public-methods # pylint: disable=protected-access,too-many-public-methods
import unittest import unittest
from unittest.mock import patch
import homeassistant.core as ha import homeassistant.core as ha
import homeassistant.loader as loader
from homeassistant.const import ( from homeassistant.const import (
STATE_ON, STATE_OFF, SERVICE_TURN_ON, SERVICE_TURN_OFF) STATE_ON, STATE_OFF, SERVICE_TURN_ON, SERVICE_TURN_OFF)
import homeassistant.components as comps import homeassistant.components as comps
from tests.common import get_test_home_assistant
class TestComponentsCore(unittest.TestCase): class TestComponentsCore(unittest.TestCase):
""" Tests homeassistant.components module. """ """ Tests homeassistant.components module. """
def setUp(self): # pylint: disable=invalid-name def setUp(self): # pylint: disable=invalid-name
""" Init needed objects. """ """ Init needed objects. """
self.hass = ha.HomeAssistant() self.hass = get_test_home_assistant()
self.assertTrue(comps.setup(self.hass, {})) self.assertTrue(comps.setup(self.hass, {}))
self.hass.states.set('light.Bowl', STATE_ON) self.hass.states.set('light.Bowl', STATE_ON)
@ -58,3 +60,24 @@ class TestComponentsCore(unittest.TestCase):
self.hass.pool.block_till_done() self.hass.pool.block_till_done()
self.assertEqual(1, len(runs)) self.assertEqual(1, len(runs))
@patch('homeassistant.core.ServiceRegistry.call')
def test_turn_on_to_not_block_for_domains_without_service(self, mock_call):
self.hass.services.register('light', SERVICE_TURN_ON, lambda x: x)
# We can't test if our service call results in services being called
# because by mocking out the call service method, we mock out all
# So we mimick how the service registry calls services
service_call = ha.ServiceCall('homeassistant', 'turn_on', {
'entity_id': ['light.test', 'sensor.bla', 'light.bla']
})
self.hass.services._services['homeassistant']['turn_on'](service_call)
self.assertEqual(2, mock_call.call_count)
self.assertEqual(
('light', 'turn_on', {'entity_id': ['light.bla', 'light.test']},
True),
mock_call.call_args_list[0][0])
self.assertEqual(
('sensor', 'turn_on', {'entity_id': ['sensor.bla']}, False),
mock_call.call_args_list[1][0])

View file

@ -0,0 +1,139 @@
"""
tests.test_component_mqtt_eventstream
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Tests MQTT eventstream component.
"""
import json
import unittest
from unittest.mock import ANY, patch
import homeassistant.components.mqtt_eventstream as eventstream
from homeassistant.const import EVENT_STATE_CHANGED
from homeassistant.core import State
from homeassistant.remote import JSONEncoder
import homeassistant.util.dt as dt_util
from tests.common import (
get_test_home_assistant,
mock_mqtt_component,
fire_mqtt_message,
mock_state_change_event,
fire_time_changed
)
class TestMqttEventStream(unittest.TestCase):
""" Test the MQTT eventstream module. """
def setUp(self): # pylint: disable=invalid-name
super(TestMqttEventStream, self).setUp()
self.hass = get_test_home_assistant()
self.mock_mqtt = mock_mqtt_component(self.hass)
def tearDown(self): # pylint: disable=invalid-name
""" Stop down stuff we started. """
self.hass.stop()
def add_eventstream(self, sub_topic=None, pub_topic=None):
""" Add a mqtt_eventstream component to the hass. """
config = {}
if sub_topic:
config['subscribe_topic'] = sub_topic
if pub_topic:
config['publish_topic'] = pub_topic
return eventstream.setup(self.hass, {eventstream.DOMAIN: config})
def test_setup_succeeds(self):
self.assertTrue(self.add_eventstream())
def test_setup_with_pub(self):
# Should start off with no listeners for all events
self.assertEqual(self.hass.bus.listeners.get('*'), None)
self.assertTrue(self.add_eventstream(pub_topic='bar'))
self.hass.pool.block_till_done()
# Verify that the event handler has been added as a listener
self.assertEqual(self.hass.bus.listeners.get('*'), 1)
@patch('homeassistant.components.mqtt.subscribe')
def test_subscribe(self, mock_sub):
sub_topic = 'foo'
self.assertTrue(self.add_eventstream(sub_topic=sub_topic))
self.hass.pool.block_till_done()
# Verify that the this entity was subscribed to the topic
mock_sub.assert_called_with(self.hass, sub_topic, ANY)
@patch('homeassistant.components.mqtt.publish')
@patch('homeassistant.core.dt_util.datetime_to_str')
def test_state_changed_event_sends_message(self, mock_datetime, mock_pub):
now = '00:19:19 11-01-2016'
e_id = 'fake.entity'
pub_topic = 'bar'
mock_datetime.return_value = now
# Add the eventstream component for publishing events
self.assertTrue(self.add_eventstream(pub_topic=pub_topic))
self.hass.pool.block_till_done()
# Reset the mock because it will have already gotten calls for the
# mqtt_eventstream state change on initialization, etc.
mock_pub.reset_mock()
# Set a state of an entity
mock_state_change_event(self.hass, State(e_id, 'on'))
self.hass.pool.block_till_done()
# The order of the JSON is indeterminate,
# so first just check that publish was called
mock_pub.assert_called_with(self.hass, pub_topic, ANY)
self.assertTrue(mock_pub.called)
# Get the actual call to publish and make sure it was the one
# we were looking for
msg = mock_pub.call_args[0][2]
event = {}
event['event_type'] = EVENT_STATE_CHANGED
new_state = {
"last_updated": now,
"state": "on",
"entity_id": e_id,
"attributes": {},
"last_changed": now
}
event['event_data'] = {"new_state": new_state, "entity_id": e_id}
# Verify that the message received was that expected
self.assertEqual(json.loads(msg), event)
@patch('homeassistant.components.mqtt.publish')
def test_time_event_does_not_send_message(self, mock_pub):
self.assertTrue(self.add_eventstream(pub_topic='bar'))
self.hass.pool.block_till_done()
# Reset the mock because it will have already gotten calls for the
# mqtt_eventstream state change on initialization, etc.
mock_pub.reset_mock()
fire_time_changed(self.hass, dt_util.utcnow())
self.assertFalse(mock_pub.called)
def test_receiving_remote_event_fires_hass_event(self):
sub_topic = 'foo'
self.assertTrue(self.add_eventstream(sub_topic=sub_topic))
self.hass.pool.block_till_done()
calls = []
self.hass.bus.listen_once('test_event', lambda _: calls.append(1))
self.hass.pool.block_till_done()
payload = json.dumps(
{'event_type': 'test_event', 'event_data': {}},
cls=JSONEncoder
)
fire_mqtt_message(self.hass, sub_topic, payload)
self.hass.pool.block_till_done()
self.assertEqual(1, len(calls))

View file

@ -0,0 +1,68 @@
"""
tests.helpers.test_service
~~~~~~~~~~~~~~~~~~~~~~~~~~
Test service helpers.
"""
import unittest
from unittest.mock import patch
from homeassistant.const import SERVICE_TURN_ON
from homeassistant.helpers import service
from tests.common import get_test_home_assistant, mock_service
class TestServiceHelpers(unittest.TestCase):
"""
Tests the Home Assistant service helpers.
"""
def setUp(self): # pylint: disable=invalid-name
""" things to be run when tests are started. """
self.hass = get_test_home_assistant()
self.calls = mock_service(self.hass, 'test_domain', 'test_service')
def tearDown(self): # pylint: disable=invalid-name
""" Stop down stuff we started. """
self.hass.stop()
def test_split_entity_string(self):
service.call_from_config(self.hass, {
'service': 'test_domain.test_service',
'entity_id': 'hello.world, sensor.beer'
})
self.hass.pool.block_till_done()
self.assertEqual(['hello.world', 'sensor.beer'],
self.calls[-1].data.get('entity_id'))
def test_not_mutate_input(self):
orig = {
'service': 'test_domain.test_service',
'entity_id': 'hello.world, sensor.beer',
'data': {
'hello': 1,
},
}
service.call_from_config(self.hass, orig)
self.hass.pool.block_till_done()
self.assertEqual({
'service': 'test_domain.test_service',
'entity_id': 'hello.world, sensor.beer',
'data': {
'hello': 1,
},
}, orig)
@patch('homeassistant.helpers.service._LOGGER.error')
def test_fail_silently_if_no_service(self, mock_log):
service.call_from_config(self.hass, None)
self.assertEqual(1, mock_log.call_count)
service.call_from_config(self.hass, {})
self.assertEqual(2, mock_log.call_count)
service.call_from_config(self.hass, {
'service': 'invalid'
})
self.assertEqual(3, mock_log.call_count)

148
tests/helpers/test_state.py Normal file
View file

@ -0,0 +1,148 @@
"""
tests.helpers.test_state
~~~~~~~~~~~~~~~~~~~~~~~~
Test state helpers.
"""
from datetime import timedelta
import unittest
from unittest.mock import patch
import homeassistant.core as ha
import homeassistant.components as core_components
from homeassistant.const import SERVICE_TURN_ON
from homeassistant.util import dt as dt_util
from homeassistant.helpers import state
from tests.common import get_test_home_assistant, mock_service
class TestStateHelpers(unittest.TestCase):
"""
Tests the Home Assistant event helpers.
"""
def setUp(self): # pylint: disable=invalid-name
""" things to be run when tests are started. """
self.hass = get_test_home_assistant()
core_components.setup(self.hass, {})
def tearDown(self): # pylint: disable=invalid-name
""" Stop down stuff we started. """
self.hass.stop()
def test_get_changed_since(self):
point1 = dt_util.utcnow()
point2 = point1 + timedelta(seconds=5)
point3 = point2 + timedelta(seconds=5)
with patch('homeassistant.core.dt_util.utcnow', return_value=point1):
self.hass.states.set('light.test', 'on')
state1 = self.hass.states.get('light.test')
with patch('homeassistant.core.dt_util.utcnow', return_value=point2):
self.hass.states.set('light.test2', 'on')
state2 = self.hass.states.get('light.test2')
with patch('homeassistant.core.dt_util.utcnow', return_value=point3):
self.hass.states.set('light.test3', 'on')
state3 = self.hass.states.get('light.test3')
self.assertEqual(
[state2, state3],
state.get_changed_since([state1, state2, state3], point2))
def test_track_states(self):
point1 = dt_util.utcnow()
point2 = point1 + timedelta(seconds=5)
point3 = point2 + timedelta(seconds=5)
with patch('homeassistant.core.dt_util.utcnow') as mock_utcnow:
mock_utcnow.return_value = point2
with state.TrackStates(self.hass) as states:
mock_utcnow.return_value = point1
self.hass.states.set('light.test', 'on')
mock_utcnow.return_value = point2
self.hass.states.set('light.test2', 'on')
state2 = self.hass.states.get('light.test2')
mock_utcnow.return_value = point3
self.hass.states.set('light.test3', 'on')
state3 = self.hass.states.get('light.test3')
self.assertEqual(
sorted([state2, state3], key=lambda state: state.entity_id),
sorted(states, key=lambda state: state.entity_id))
def test_reproduce_state_with_turn_on(self):
calls = mock_service(self.hass, 'light', SERVICE_TURN_ON)
self.hass.states.set('light.test', 'off')
state.reproduce_state(self.hass, ha.State('light.test', 'on'))
self.hass.pool.block_till_done()
self.assertTrue(len(calls) > 0)
last_call = calls[-1]
self.assertEqual('light', last_call.domain)
self.assertEqual(SERVICE_TURN_ON, last_call.service)
self.assertEqual(['light.test'], last_call.data.get('entity_id'))
def test_reproduce_state_with_complex_service_data(self):
calls = mock_service(self.hass, 'light', SERVICE_TURN_ON)
self.hass.states.set('light.test', 'off')
complex_data = ['hello', {'11': '22'}]
state.reproduce_state(self.hass, ha.State('light.test', 'on', {
'complex': complex_data
}))
self.hass.pool.block_till_done()
self.assertTrue(len(calls) > 0)
last_call = calls[-1]
self.assertEqual('light', last_call.domain)
self.assertEqual(SERVICE_TURN_ON, last_call.service)
self.assertEqual(complex_data, last_call.data.get('complex'))
def test_reproduce_state_with_group(self):
light_calls = mock_service(self.hass, 'light', SERVICE_TURN_ON)
self.hass.states.set('group.test', 'off', {
'entity_id': ['light.test1', 'light.test2']})
state.reproduce_state(self.hass, ha.State('group.test', 'on'))
self.hass.pool.block_till_done()
self.assertEqual(1, len(light_calls))
last_call = light_calls[-1]
self.assertEqual('light', last_call.domain)
self.assertEqual(SERVICE_TURN_ON, last_call.service)
self.assertEqual(['light.test1', 'light.test2'],
last_call.data.get('entity_id'))
def test_reproduce_state_group_states_with_same_domain_and_data(self):
light_calls = mock_service(self.hass, 'light', SERVICE_TURN_ON)
self.hass.states.set('light.test1', 'off')
self.hass.states.set('light.test2', 'off')
state.reproduce_state(self.hass, [
ha.State('light.test1', 'on', {'brightness': 95}),
ha.State('light.test2', 'on', {'brightness': 95})])
self.hass.pool.block_till_done()
self.assertEqual(1, len(light_calls))
last_call = light_calls[-1]
self.assertEqual('light', last_call.domain)
self.assertEqual(SERVICE_TURN_ON, last_call.service)
self.assertEqual(['light.test1', 'light.test2'],
last_call.data.get('entity_id'))
self.assertEqual(95, last_call.data.get('brightness'))