mirror of
https://github.com/home-assistant/core.git
synced 2025-07-22 20:57:21 +00:00
Merge branch 'dev' into mysensors-component-switch
This commit is contained in:
commit
0b3a66dd1a
@ -48,7 +48,6 @@ omit =
|
||||
homeassistant/components/device_tracker/asuswrt.py
|
||||
homeassistant/components/device_tracker/ddwrt.py
|
||||
homeassistant/components/device_tracker/fritz.py
|
||||
homeassistant/components/device_tracker/locative.py
|
||||
homeassistant/components/device_tracker/icloud.py
|
||||
homeassistant/components/device_tracker/luci.py
|
||||
homeassistant/components/device_tracker/netgear.py
|
||||
|
@ -8,9 +8,7 @@ python:
|
||||
- 3.4
|
||||
- 3.5
|
||||
install:
|
||||
# Validate requirements_all.txt on Python 3.4
|
||||
- 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
|
||||
- "true"
|
||||
script:
|
||||
- script/cibuild
|
||||
matrix:
|
||||
|
@ -87,13 +87,21 @@ def setup(hass, config):
|
||||
lambda item: util.split_entity_id(item)[0])
|
||||
|
||||
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
|
||||
data = dict(service.data)
|
||||
|
||||
# ent_ids is a generator, convert it to a list.
|
||||
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_ON, handle_turn_service)
|
||||
|
@ -67,7 +67,7 @@ class VerisureAlarm(alarm.AlarmControlPanel):
|
||||
self._state = STATE_ALARM_DISARMED
|
||||
elif verisure.ALARM_STATUS[self._id].status == 'armedhome':
|
||||
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
|
||||
elif verisure.ALARM_STATUS[self._id].status != 'pending':
|
||||
_LOGGER.error(
|
||||
|
@ -11,6 +11,7 @@ import logging
|
||||
|
||||
from homeassistant.const import HTTP_OK, HTTP_UNPROCESSABLE_ENTITY
|
||||
from homeassistant.util import template
|
||||
from homeassistant.helpers.service import call_from_config
|
||||
|
||||
DOMAIN = 'alexa'
|
||||
DEPENDENCIES = ['http']
|
||||
@ -23,6 +24,7 @@ API_ENDPOINT = '/api/alexa'
|
||||
CONF_INTENTS = 'intents'
|
||||
CONF_CARD = 'card'
|
||||
CONF_SPEECH = 'speech'
|
||||
CONF_ACTION = 'action'
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
@ -80,6 +82,7 @@ def _handle_alexa(handler, path_match, data):
|
||||
|
||||
speech = config.get(CONF_SPEECH)
|
||||
card = config.get(CONF_CARD)
|
||||
action = config.get(CONF_ACTION)
|
||||
|
||||
# pylint: disable=unsubscriptable-object
|
||||
if speech is not None:
|
||||
@ -89,6 +92,9 @@ def _handle_alexa(handler, path_match, data):
|
||||
response.add_card(CardType[card['type']], card['title'],
|
||||
card['content'])
|
||||
|
||||
if action is not None:
|
||||
call_from_config(handler.server.hass, action, True)
|
||||
|
||||
handler.write_json(response.as_dict())
|
||||
|
||||
|
||||
|
@ -9,9 +9,9 @@ https://home-assistant.io/components/automation/
|
||||
import logging
|
||||
|
||||
from homeassistant.bootstrap import prepare_setup_platform
|
||||
from homeassistant.util import split_entity_id
|
||||
from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM
|
||||
from homeassistant.const import CONF_PLATFORM
|
||||
from homeassistant.components import logbook
|
||||
from homeassistant.helpers.service import call_from_config
|
||||
|
||||
DOMAIN = 'automation'
|
||||
|
||||
@ -19,8 +19,6 @@ DEPENDENCIES = ['group']
|
||||
|
||||
CONF_ALIAS = 'alias'
|
||||
CONF_SERVICE = 'service'
|
||||
CONF_SERVICE_ENTITY_ID = 'entity_id'
|
||||
CONF_SERVICE_DATA = 'data'
|
||||
|
||||
CONF_CONDITION = 'condition'
|
||||
CONF_ACTION = 'action'
|
||||
@ -96,22 +94,7 @@ def _get_action(hass, config, name):
|
||||
_LOGGER.info('Executing %s', name)
|
||||
logbook.log_entry(hass, name, 'has been triggered', DOMAIN)
|
||||
|
||||
domain, service = split_entity_id(config[CONF_SERVICE])
|
||||
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)
|
||||
call_from_config(hass, config)
|
||||
|
||||
return action
|
||||
|
||||
|
@ -82,21 +82,21 @@ def if_action(hass, config):
|
||||
if before is None:
|
||||
before_func = lambda: None
|
||||
elif before == EVENT_SUNRISE:
|
||||
before_func = lambda: sun.next_rising_utc(hass) + before_offset
|
||||
before_func = lambda: sun.next_rising(hass) + before_offset
|
||||
else:
|
||||
before_func = lambda: sun.next_setting_utc(hass) + before_offset
|
||||
before_func = lambda: sun.next_setting(hass) + before_offset
|
||||
|
||||
if after is None:
|
||||
after_func = lambda: None
|
||||
elif after == EVENT_SUNRISE:
|
||||
after_func = lambda: sun.next_rising_utc(hass) + after_offset
|
||||
after_func = lambda: sun.next_rising(hass) + after_offset
|
||||
else:
|
||||
after_func = lambda: sun.next_setting_utc(hass) + after_offset
|
||||
after_func = lambda: sun.next_setting(hass) + after_offset
|
||||
|
||||
def time_if():
|
||||
""" Validate time based if-condition """
|
||||
|
||||
now = dt_util.utcnow()
|
||||
now = dt_util.now()
|
||||
before = before_func()
|
||||
after = after_func()
|
||||
|
||||
|
@ -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
|
||||
https://home-assistant.io/components/binary_sensor.rest/
|
||||
"""
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import requests
|
||||
|
||||
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
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -19,60 +18,33 @@ _LOGGER = logging.getLogger(__name__)
|
||||
DEFAULT_NAME = 'REST Binary Sensor'
|
||||
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
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Get the REST binary sensor. """
|
||||
|
||||
use_get = False
|
||||
use_post = False
|
||||
|
||||
"""Setup REST binary sensors."""
|
||||
resource = config.get('resource', None)
|
||||
method = config.get('method', DEFAULT_METHOD)
|
||||
payload = config.get('payload', None)
|
||||
verify_ssl = config.get('verify_ssl', True)
|
||||
|
||||
if method == 'GET':
|
||||
use_get = True
|
||||
elif method == 'POST':
|
||||
use_post = True
|
||||
rest = RestData(method, resource, payload, verify_ssl)
|
||||
rest.update()
|
||||
|
||||
try:
|
||||
if use_get:
|
||||
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)
|
||||
if rest.data is None:
|
||||
_LOGGER.error('Unable to fetch Rest data')
|
||||
return False
|
||||
|
||||
if use_get:
|
||||
rest = RestDataGet(resource, verify_ssl)
|
||||
elif use_post:
|
||||
rest = RestDataPost(resource, payload, verify_ssl)
|
||||
|
||||
add_devices([RestBinarySensor(hass,
|
||||
rest,
|
||||
config.get('name', DEFAULT_NAME),
|
||||
config.get(CONF_VALUE_TEMPLATE))])
|
||||
add_devices([RestBinarySensor(
|
||||
hass, rest, config.get('name', DEFAULT_NAME),
|
||||
config.get(CONF_VALUE_TEMPLATE))])
|
||||
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
class RestBinarySensor(BinarySensorDevice):
|
||||
""" Implements a REST binary sensor. """
|
||||
"""REST binary sensor."""
|
||||
|
||||
def __init__(self, hass, rest, name, value_template):
|
||||
"""Initialize a REST binary sensor."""
|
||||
self._hass = hass
|
||||
self.rest = rest
|
||||
self._name = name
|
||||
@ -82,63 +54,20 @@ class RestBinarySensor(BinarySensorDevice):
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" The name of the binary sensor. """
|
||||
"""Name of the binary sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
""" True if the binary sensor is on. """
|
||||
if self.rest.data is False:
|
||||
"""Return if the binary sensor is on."""
|
||||
if self.rest.data is None:
|
||||
return False
|
||||
else:
|
||||
if self._value_template is not None:
|
||||
self.rest.data = template.render_with_possible_json_value(
|
||||
self._hass, self._value_template, self.rest.data, False)
|
||||
return bool(int(self.rest.data))
|
||||
|
||||
if self._value_template is not None:
|
||||
self.rest.data = template.render_with_possible_json_value(
|
||||
self._hass, self._value_template, self.rest.data, False)
|
||||
return bool(int(self.rest.data))
|
||||
|
||||
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()
|
||||
|
||||
|
||||
# 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
|
||||
|
@ -6,65 +6,100 @@ Locative platform for the device tracker.
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/device_tracker.locative/
|
||||
"""
|
||||
import logging
|
||||
from functools import partial
|
||||
|
||||
from homeassistant.const import (
|
||||
HTTP_UNPROCESSABLE_ENTITY, HTTP_INTERNAL_SERVER_ERROR)
|
||||
HTTP_UNPROCESSABLE_ENTITY, STATE_NOT_HOME)
|
||||
from homeassistant.components.device_tracker import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEPENDENCIES = ['http']
|
||||
|
||||
_SEE = 0
|
||||
|
||||
URL_API_LOCATIVE_ENDPOINT = "/api/locative"
|
||||
|
||||
|
||||
def setup_scanner(hass, config, see):
|
||||
""" Set up an endpoint for the Locative app. """
|
||||
|
||||
# Use a global variable to keep setup_scanner compact when using a callback
|
||||
global _SEE
|
||||
_SEE = see
|
||||
|
||||
# POST would be semantically better, but that currently does not work
|
||||
# since Locative sends the data as key1=value1&key2=value2
|
||||
# in the request body, while Home Assistant expects json there.
|
||||
|
||||
hass.http.register_path(
|
||||
'GET', URL_API_LOCATIVE_ENDPOINT, _handle_get_api_locative)
|
||||
'GET', URL_API_LOCATIVE_ENDPOINT,
|
||||
partial(_handle_get_api_locative, hass, see))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _handle_get_api_locative(handler, path_match, data):
|
||||
def _handle_get_api_locative(hass, see, handler, path_match, data):
|
||||
""" Locative message received. """
|
||||
|
||||
if not isinstance(data, dict):
|
||||
handler.write_json_message(
|
||||
"Error while parsing Locative message.",
|
||||
HTTP_INTERNAL_SERVER_ERROR)
|
||||
if not _check_data(handler, data):
|
||||
return
|
||||
|
||||
device = data['device'].replace('-', '')
|
||||
location_name = data['id'].lower()
|
||||
direction = data['trigger']
|
||||
|
||||
if direction == 'enter':
|
||||
see(dev_id=device, location_name=location_name)
|
||||
handler.write_text("Setting location to {}".format(location_name))
|
||||
|
||||
elif direction == 'exit':
|
||||
current_state = hass.states.get(
|
||||
"{}.{}".format(DOMAIN, device)).state
|
||||
|
||||
if current_state == location_name:
|
||||
see(dev_id=device, location_name=STATE_NOT_HOME)
|
||||
handler.write_text("Setting location to not home")
|
||||
else:
|
||||
# Ignore the message if it is telling us to exit a zone that we
|
||||
# aren't currently in. This occurs when a zone is entered before
|
||||
# the previous zone was exited. The enter message will be sent
|
||||
# first, then the exit message will be sent second.
|
||||
handler.write_text(
|
||||
'Ignoring exit from {} (already in {})'.format(
|
||||
location_name, current_state))
|
||||
|
||||
elif direction == 'test':
|
||||
# In the app, a test message can be sent. Just return something to
|
||||
# the user to let them know that it works.
|
||||
handler.write_text("Received test message.")
|
||||
|
||||
else:
|
||||
handler.write_text(
|
||||
"Received unidentified message: {}".format(direction),
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
_LOGGER.error("Received unidentified message from Locative: %s",
|
||||
direction)
|
||||
|
||||
|
||||
def _check_data(handler, data):
|
||||
if 'latitude' not in data or 'longitude' not in data:
|
||||
handler.write_json_message(
|
||||
"Location not specified.",
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
return
|
||||
if 'device' not in data or 'id' not in data:
|
||||
handler.write_json_message(
|
||||
"Device id or location id not specified.",
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
return
|
||||
handler.write_text("Latitude and longitude not specified.",
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
_LOGGER.error("Latitude and longitude not specified.")
|
||||
return False
|
||||
|
||||
try:
|
||||
gps_coords = (float(data['latitude']), float(data['longitude']))
|
||||
except ValueError:
|
||||
# If invalid latitude / longitude format
|
||||
handler.write_json_message(
|
||||
"Invalid latitude / longitude format.",
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
return
|
||||
if 'device' not in data:
|
||||
handler.write_text("Device id not specified.",
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
_LOGGER.error("Device id not specified.")
|
||||
return False
|
||||
|
||||
# entity id's in Home Assistant must be alphanumerical
|
||||
device_uuid = data['device']
|
||||
device_entity_id = device_uuid.replace('-', '')
|
||||
if 'id' not in data:
|
||||
handler.write_text("Location id not specified.",
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
_LOGGER.error("Location id not specified.")
|
||||
return False
|
||||
|
||||
_SEE(dev_id=device_entity_id, gps=gps_coords, location_name=data['id'])
|
||||
if 'trigger' not in data:
|
||||
handler.write_text("Trigger is not specified.",
|
||||
HTTP_UNPROCESSABLE_ENTITY)
|
||||
_LOGGER.error("Trigger is not specified.")
|
||||
return False
|
||||
|
||||
handler.write_json_message("Locative message processed")
|
||||
return True
|
||||
|
@ -19,7 +19,7 @@ from homeassistant.components.device_tracker import DOMAIN
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=5)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
REQUIREMENTS = ['pynetgear==0.3']
|
||||
REQUIREMENTS = ['pynetgear==0.3.1']
|
||||
|
||||
|
||||
def get_scanner(hass, config):
|
||||
|
@ -10,14 +10,17 @@ import json
|
||||
import logging
|
||||
|
||||
import homeassistant.components.mqtt as mqtt
|
||||
from homeassistant.const import (STATE_HOME, STATE_NOT_HOME)
|
||||
|
||||
DEPENDENCIES = ['mqtt']
|
||||
|
||||
CONF_TRANSITION_EVENTS = 'use_events'
|
||||
LOCATION_TOPIC = 'owntracks/+/+'
|
||||
EVENT_TOPIC = 'owntracks/+/+/event'
|
||||
|
||||
|
||||
def setup_scanner(hass, config, see):
|
||||
""" Set up a OwnTracksks tracker. """
|
||||
""" Set up an OwnTracks tracker. """
|
||||
|
||||
def owntracks_location_update(topic, payload, qos):
|
||||
""" MQTT message received. """
|
||||
@ -48,6 +51,56 @@ def setup_scanner(hass, config, see):
|
||||
|
||||
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'] == '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
|
||||
|
@ -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'
|
@ -1,2 +1,2 @@
|
||||
""" DO NOT MODIFY. Auto-generated by build_frontend script """
|
||||
VERSION = "be08c5a3ce12040bbdba2db83cb1a568"
|
||||
VERSION = "72a8220d0db0f7f3702228cd556b8c40"
|
||||
|
File diff suppressed because one or more lines are too long
@ -1 +1 @@
|
||||
Subproject commit 50aadaf880a9cb36bf144540171ff5fa029e9eaf
|
||||
Subproject commit 78c348cb7b0a60ba015e3b652e538155d3e94a11
|
@ -21,7 +21,7 @@ from urllib.parse import urlparse, parse_qs
|
||||
|
||||
import homeassistant.core as ha
|
||||
from homeassistant.const import (
|
||||
SERVER_PORT, CONTENT_TYPE_JSON,
|
||||
SERVER_PORT, CONTENT_TYPE_JSON, CONTENT_TYPE_TEXT_PLAIN,
|
||||
HTTP_HEADER_HA_AUTH, HTTP_HEADER_CONTENT_TYPE, HTTP_HEADER_ACCEPT_ENCODING,
|
||||
HTTP_HEADER_CONTENT_ENCODING, HTTP_HEADER_VARY, HTTP_HEADER_CONTENT_LENGTH,
|
||||
HTTP_HEADER_CACHE_CONTROL, HTTP_HEADER_EXPIRES, HTTP_OK, HTTP_UNAUTHORIZED,
|
||||
@ -293,6 +293,17 @@ class RequestHandler(SimpleHTTPRequestHandler):
|
||||
json.dumps(data, indent=4, sort_keys=True,
|
||||
cls=rem.JSONEncoder).encode("UTF-8"))
|
||||
|
||||
def write_text(self, message, status_code=HTTP_OK):
|
||||
""" Helper method to return a text message to the caller. """
|
||||
self.send_response(status_code)
|
||||
self.send_header(HTTP_HEADER_CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN)
|
||||
|
||||
self.set_session_cookie_header()
|
||||
|
||||
self.end_headers()
|
||||
|
||||
self.wfile.write(message.encode("UTF-8"))
|
||||
|
||||
def write_file(self, path, cache_headers=True):
|
||||
""" Returns a file to the user. """
|
||||
try:
|
||||
|
@ -7,7 +7,6 @@ For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/influxdb/
|
||||
"""
|
||||
import logging
|
||||
|
||||
import homeassistant.util as util
|
||||
from homeassistant.helpers import validate_config
|
||||
from homeassistant.const import (EVENT_STATE_CHANGED, STATE_ON, STATE_OFF,
|
||||
@ -77,6 +76,10 @@ def setup(hass, config):
|
||||
_state = 0
|
||||
else:
|
||||
_state = state.state
|
||||
try:
|
||||
_state = float(_state)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
measurement = state.attributes.get('unit_of_measurement', state.domain)
|
||||
|
||||
|
@ -17,7 +17,7 @@ from urllib.parse import urlparse
|
||||
from homeassistant.loader import get_component
|
||||
import homeassistant.util as 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 (
|
||||
Light, ATTR_BRIGHTNESS, ATTR_XY_COLOR, ATTR_COLOR_TEMP,
|
||||
ATTR_TRANSITION, ATTR_FLASH, FLASH_LONG, FLASH_SHORT,
|
||||
@ -35,9 +35,9 @@ _CONFIGURING = {}
|
||||
_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. """
|
||||
path = hass.config.path(PHUE_CONFIG_FILE)
|
||||
path = hass.config.path(filename)
|
||||
|
||||
if not os.path.isfile(path):
|
||||
return None
|
||||
@ -54,13 +54,14 @@ def _find_host_from_config(hass):
|
||||
|
||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
""" Gets the Hue lights. """
|
||||
filename = config.get(CONF_FILENAME, PHUE_CONFIG_FILE)
|
||||
if discovery_info is not None:
|
||||
host = urlparse(discovery_info[1]).hostname
|
||||
else:
|
||||
host = config.get(CONF_HOST, None)
|
||||
|
||||
if host is None:
|
||||
host = _find_host_from_config(hass)
|
||||
host = _find_host_from_config(hass, filename)
|
||||
|
||||
if host is None:
|
||||
_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:
|
||||
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. """
|
||||
import phue
|
||||
|
||||
try:
|
||||
bridge = phue.Bridge(
|
||||
host,
|
||||
config_file_path=hass.config.path(PHUE_CONFIG_FILE))
|
||||
config_file_path=hass.config.path(filename))
|
||||
except ConnectionRefusedError: # Wrong host was given
|
||||
_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:
|
||||
_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
|
||||
|
||||
@ -121,10 +122,17 @@ def setup_bridge(host, hass, add_devices_callback):
|
||||
|
||||
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():
|
||||
if light_id not in lights:
|
||||
lights[light_id] = HueLight(int(light_id), info,
|
||||
bridge, update_lights)
|
||||
bridge, update_lights,
|
||||
bridge_type=bridge_type)
|
||||
new_lights.append(lights[light_id])
|
||||
else:
|
||||
lights[light_id].info = info
|
||||
@ -135,7 +143,7 @@ def setup_bridge(host, hass, add_devices_callback):
|
||||
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. """
|
||||
configurator = get_component('configurator')
|
||||
|
||||
@ -149,7 +157,7 @@ def request_configuration(host, hass, add_devices_callback):
|
||||
# pylint: disable=unused-argument
|
||||
def hue_configuration_callback(data):
|
||||
""" 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(
|
||||
hass, "Philips Hue", hue_configuration_callback,
|
||||
@ -163,11 +171,14 @@ def request_configuration(host, hass, add_devices_callback):
|
||||
class HueLight(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.info = info
|
||||
self.bridge = bridge
|
||||
self.update_lights = update_lights
|
||||
self.bridge_type = bridge_type
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
@ -227,7 +238,7 @@ class HueLight(Light):
|
||||
command['alert'] = 'lselect'
|
||||
elif flash == FLASH_SHORT:
|
||||
command['alert'] = 'select'
|
||||
else:
|
||||
elif self.bridge_type == 'hue':
|
||||
command['alert'] = 'none'
|
||||
|
||||
effect = kwargs.get(ATTR_EFFECT)
|
||||
@ -237,7 +248,7 @@ class HueLight(Light):
|
||||
elif effect == EFFECT_RANDOM:
|
||||
command['hue'] = random.randrange(0, 65535)
|
||||
command['sat'] = random.randrange(150, 254)
|
||||
else:
|
||||
elif self.bridge_type == 'hue':
|
||||
command['effect'] = 'none'
|
||||
|
||||
self.bridge.set_light(self.light_id, command)
|
||||
|
@ -14,9 +14,9 @@ from homeassistant.components.switch.vera import VeraSwitch
|
||||
|
||||
from homeassistant.components.light import ATTR_BRIGHTNESS
|
||||
|
||||
REQUIREMENTS = ['https://github.com/pavoni/home-assistant-vera-api/archive/'
|
||||
'efdba4e63d58a30bc9b36d9e01e69858af9130b8.zip'
|
||||
'#python-vera==0.1.1']
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
|
||||
REQUIREMENTS = ['pyvera==0.2.2']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -36,10 +36,19 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
|
||||
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 = []
|
||||
try:
|
||||
devices = controller.get_devices([
|
||||
devices = vera_controller.get_devices([
|
||||
'Switch',
|
||||
'On/Off Switch',
|
||||
'Dimmable Switch'])
|
||||
@ -54,7 +63,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
exclude = extra_data.get('exclude', False)
|
||||
|
||||
if exclude is not True:
|
||||
lights.append(VeraLight(device, extra_data))
|
||||
lights.append(VeraLight(device, vera_controller, extra_data))
|
||||
|
||||
add_devices_callback(lights)
|
||||
|
||||
|
@ -20,7 +20,7 @@ from homeassistant.components.media_player import (
|
||||
SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK,
|
||||
MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO)
|
||||
|
||||
REQUIREMENTS = ['pychromecast==0.6.13']
|
||||
REQUIREMENTS = ['pychromecast==0.6.14']
|
||||
CONF_IGNORE_CEC = 'ignore_cec'
|
||||
CAST_SPLASH = 'https://home-assistant.io/images/cast/splash.png'
|
||||
SUPPORT_CAST = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
|
||||
|
@ -142,10 +142,14 @@ class MpdDevice(MediaPlayerDevice):
|
||||
def media_title(self):
|
||||
""" Title of current playing media. """
|
||||
name = self.currentsong.get('name', None)
|
||||
title = self.currentsong['title']
|
||||
title = self.currentsong.get('title', None)
|
||||
|
||||
if name is None:
|
||||
if name is None and title is None:
|
||||
return "None"
|
||||
elif name is None:
|
||||
return title
|
||||
elif title is None:
|
||||
return name
|
||||
else:
|
||||
return '{}: {}'.format(name, title)
|
||||
|
||||
|
@ -35,7 +35,7 @@ SUPPORT_PLEX = SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK
|
||||
|
||||
|
||||
def config_from_file(filename, config=None):
|
||||
''' Small configuration file management function'''
|
||||
""" Small configuration file management function. """
|
||||
if config:
|
||||
# We're writing configuration
|
||||
try:
|
||||
@ -85,7 +85,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
|
||||
# pylint: disable=too-many-branches
|
||||
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.exceptions
|
||||
|
||||
|
@ -149,9 +149,9 @@ class MQTT(object):
|
||||
}
|
||||
|
||||
if client_id is None:
|
||||
self._mqttc = mqtt.Client()
|
||||
self._mqttc = mqtt.Client(protocol=mqtt.MQTTv311)
|
||||
else:
|
||||
self._mqttc = mqtt.Client(client_id)
|
||||
self._mqttc = mqtt.Client(client_id, protocol=mqtt.MQTTv311)
|
||||
|
||||
self._mqttc.user_data_set(self.userdata)
|
||||
|
||||
|
@ -70,5 +70,8 @@ class EliqSensor(Entity):
|
||||
|
||||
def update(self):
|
||||
""" Gets the latest data. """
|
||||
response = self.api.get_data_now(channelid=self.channel_id)
|
||||
self._state = int(response.power)
|
||||
try:
|
||||
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
|
||||
|
@ -13,14 +13,14 @@ from homeassistant.util import Throttle
|
||||
from homeassistant.const import (CONF_API_KEY, TEMP_CELCIUS, TEMP_FAHRENHEIT)
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
REQUIREMENTS = ['pyowm==2.2.1']
|
||||
REQUIREMENTS = ['pyowm==2.3.0']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
SENSOR_TYPES = {
|
||||
'weather': ['Condition', ''],
|
||||
'temperature': ['Temperature', ''],
|
||||
'wind_speed': ['Wind speed', 'm/s'],
|
||||
'humidity': ['Humidity', '%'],
|
||||
'pressure': ['Pressure', 'hPa'],
|
||||
'pressure': ['Pressure', 'mbar'],
|
||||
'clouds': ['Cloud coverage', '%'],
|
||||
'rain': ['Rain', 'mm'],
|
||||
'snow': ['Snow', 'mm']
|
||||
|
@ -26,47 +26,21 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
|
||||
# pylint: disable=unused-variable
|
||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
""" Get the REST sensor. """
|
||||
|
||||
use_get = False
|
||||
use_post = False
|
||||
|
||||
resource = config.get('resource', None)
|
||||
method = config.get('method', DEFAULT_METHOD)
|
||||
payload = config.get('payload', None)
|
||||
verify_ssl = config.get('verify_ssl', True)
|
||||
|
||||
if method == 'GET':
|
||||
use_get = True
|
||||
elif method == 'POST':
|
||||
use_post = True
|
||||
rest = RestData(method, resource, payload, verify_ssl)
|
||||
rest.update()
|
||||
|
||||
try:
|
||||
if use_get:
|
||||
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)
|
||||
if rest.data is None:
|
||||
_LOGGER.error('Unable to fetch Rest data')
|
||||
return False
|
||||
|
||||
if use_get:
|
||||
rest = RestDataGet(resource, verify_ssl)
|
||||
elif use_post:
|
||||
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))])
|
||||
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
|
||||
@ -112,11 +86,11 @@ class RestSensor(Entity):
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class RestDataGet(object):
|
||||
""" Class for handling the data retrieval with GET method. """
|
||||
class RestData(object):
|
||||
"""Class for handling the data retrieval."""
|
||||
|
||||
def __init__(self, resource, verify_ssl):
|
||||
self._resource = resource
|
||||
def __init__(self, method, resource, data, verify_ssl):
|
||||
self._request = requests.Request(method, resource, data=data).prepare()
|
||||
self._verify_ssl = verify_ssl
|
||||
self.data = None
|
||||
|
||||
@ -124,31 +98,11 @@ class RestDataGet(object):
|
||||
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)
|
||||
with requests.Session() as sess:
|
||||
response = sess.send(self._request, 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
|
||||
|
||||
|
||||
# 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)
|
||||
except requests.exceptions.RequestException:
|
||||
_LOGGER.error("Error fetching data: %s", self._request)
|
||||
self.data = None
|
||||
|
@ -22,10 +22,22 @@ DEPENDENCIES = ['tellduslive']
|
||||
|
||||
SENSOR_TYPE_TEMP = "temp"
|
||||
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_TYPE_TEMP: ['Temperature', TEMP_CELCIUS, "mdi:thermometer"],
|
||||
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', ""],
|
||||
}
|
||||
|
||||
|
||||
|
@ -13,11 +13,9 @@ import homeassistant.util.dt as dt_util
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.const import (
|
||||
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/'
|
||||
'efdba4e63d58a30bc9b36d9e01e69858af9130b8.zip'
|
||||
'#python-vera==0.1.1']
|
||||
REQUIREMENTS = ['pyvera==0.2.2']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -37,7 +35,16 @@ def get_devices(hass, config):
|
||||
|
||||
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']
|
||||
devices = []
|
||||
try:
|
||||
@ -53,7 +60,8 @@ def get_devices(hass, config):
|
||||
exclude = extra_data.get('exclude', False)
|
||||
|
||||
if exclude is not True:
|
||||
vera_sensors.append(VeraSensor(device, extra_data))
|
||||
vera_sensors.append(
|
||||
VeraSensor(device, vera_controller, extra_data))
|
||||
|
||||
return vera_sensors
|
||||
|
||||
@ -66,8 +74,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
class VeraSensor(Entity):
|
||||
""" 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.controller = controller
|
||||
self.extra_data = extra_data
|
||||
if self.extra_data and self.extra_data.get('name'):
|
||||
self._name = self.extra_data.get('name')
|
||||
@ -76,6 +85,16 @@ class VeraSensor(Entity):
|
||||
self.current_value = ''
|
||||
self._temperature_units = None
|
||||
|
||||
self.controller.register(vera_device)
|
||||
self.controller.on(
|
||||
vera_device, self._update_callback)
|
||||
|
||||
def _update_callback(self, _device):
|
||||
""" Called by the vera device callback to update state. """
|
||||
_LOGGER.info(
|
||||
'Subscription update for %s', self.name)
|
||||
self.update_ha_state(True)
|
||||
|
||||
def __str__(self):
|
||||
return "%s %s %s" % (self.name, self.vera_device.deviceId, self.state)
|
||||
|
||||
@ -117,6 +136,11 @@ class VeraSensor(Entity):
|
||||
attr['Vera Device Id'] = self.vera_device.vera_device_id
|
||||
return attr
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
""" Tells Home Assistant not to poll this entity. """
|
||||
return False
|
||||
|
||||
def update(self):
|
||||
if self.vera_device.category == "Temperature Sensor":
|
||||
self.vera_device.refresh_value('CurrentTemperature')
|
||||
|
@ -1,39 +1,10 @@
|
||||
"""
|
||||
homeassistant.components.sensor.yr
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Yr.no weather service.
|
||||
|
||||
Configuration:
|
||||
|
||||
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
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/sensor.yr/
|
||||
"""
|
||||
import logging
|
||||
|
||||
@ -54,7 +25,7 @@ SENSOR_TYPES = {
|
||||
'precipitation': ['Condition', 'mm'],
|
||||
'temperature': ['Temperature', '°C'],
|
||||
'windSpeed': ['Wind speed', 'm/s'],
|
||||
'pressure': ['Pressure', 'hPa'],
|
||||
'pressure': ['Pressure', 'mbar'],
|
||||
'windDirection': ['Wind direction', '°'],
|
||||
'humidity': ['Humidity', '%'],
|
||||
'fog': ['Fog', '%'],
|
||||
@ -67,7 +38,7 @@ SENSOR_TYPES = {
|
||||
|
||||
|
||||
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):
|
||||
_LOGGER.error("Latitude or longitude not set in Home Assistant config")
|
||||
|
@ -18,7 +18,6 @@ from homeassistant.helpers.entity import Entity
|
||||
REQUIREMENTS = ['astral==0.8.1']
|
||||
DOMAIN = "sun"
|
||||
ENTITY_ID = "sun.sun"
|
||||
ENTITY_ID_ELEVATION = "sun.elevation"
|
||||
|
||||
CONF_ELEVATION = 'elevation'
|
||||
|
||||
@ -33,21 +32,21 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
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
|
||||
|
||||
return hass.states.is_state(entity_id, STATE_ABOVE_HORIZON)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
return dt_util.as_local(utc_next) if utc_next else 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
|
||||
|
||||
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):
|
||||
""" 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)
|
||||
|
||||
return dt_util.as_local(utc_next) if utc_next else 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
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
@ -84,7 +83,7 @@ def next_rising_utc(hass, entity_id=None):
|
||||
|
||||
|
||||
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):
|
||||
_LOGGER.error("Latitude or longitude not set in Home Assistant config")
|
||||
return False
|
||||
@ -125,7 +124,7 @@ def setup(hass, config):
|
||||
|
||||
|
||||
class Sun(Entity):
|
||||
""" Represents the Sun. """
|
||||
"""Represents the Sun."""
|
||||
|
||||
entity_id = ENTITY_ID
|
||||
|
||||
@ -158,12 +157,12 @@ class Sun(Entity):
|
||||
|
||||
@property
|
||||
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)
|
||||
|
||||
@property
|
||||
def solar_elevation(self):
|
||||
""" Returns the angle the sun is above the horizon"""
|
||||
"""Angle the sun is above the horizon."""
|
||||
from astral import Astral
|
||||
return Astral().solar_elevation(
|
||||
dt_util.utcnow(),
|
||||
|
@ -10,6 +10,8 @@ import logging
|
||||
import subprocess
|
||||
|
||||
from homeassistant.components.switch import SwitchDevice
|
||||
from homeassistant.const import CONF_VALUE_TEMPLATE
|
||||
from homeassistant.util import template
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -24,20 +26,30 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||
for dev_name, properties in switches.items():
|
||||
devices.append(
|
||||
CommandSwitch(
|
||||
hass,
|
||||
properties.get('name', dev_name),
|
||||
properties.get('oncmd', 'true'),
|
||||
properties.get('offcmd', 'true')))
|
||||
properties.get('offcmd', 'true'),
|
||||
properties.get('statecmd', False),
|
||||
properties.get(CONF_VALUE_TEMPLATE, False)))
|
||||
|
||||
add_devices_callback(devices)
|
||||
|
||||
|
||||
class CommandSwitch(SwitchDevice):
|
||||
""" Represents a switch that can be togggled using shell commands. """
|
||||
def __init__(self, name, command_on, command_off):
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def __init__(self, hass, name, command_on, command_off,
|
||||
command_state, value_template):
|
||||
|
||||
self._hass = hass
|
||||
self._name = name
|
||||
self._state = False
|
||||
self._command_on = command_on
|
||||
self._command_off = command_off
|
||||
self._command_state = command_state
|
||||
self._value_template = value_template
|
||||
|
||||
@staticmethod
|
||||
def _switch(command):
|
||||
@ -51,10 +63,27 @@ class CommandSwitch(SwitchDevice):
|
||||
|
||||
return success
|
||||
|
||||
@staticmethod
|
||||
def _query_state_value(command):
|
||||
""" Execute state command for return value. """
|
||||
_LOGGER.info('Running state command: %s', command)
|
||||
|
||||
try:
|
||||
return_value = subprocess.check_output(command, shell=True)
|
||||
return return_value.strip().decode('utf-8')
|
||||
except subprocess.CalledProcessError:
|
||||
_LOGGER.error('Command failed: %s', command)
|
||||
|
||||
@staticmethod
|
||||
def _query_state_code(command):
|
||||
""" Execute state command for return code. """
|
||||
_LOGGER.info('Running state command: %s', command)
|
||||
return subprocess.call(command, shell=True) == 0
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
""" No polling needed. """
|
||||
return False
|
||||
""" Only poll if we have statecmd. """
|
||||
return self._command_state is not None
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@ -66,14 +95,34 @@ class CommandSwitch(SwitchDevice):
|
||||
""" True if device is on. """
|
||||
return self._state
|
||||
|
||||
def _query_state(self):
|
||||
""" Query for state. """
|
||||
if not self._command_state:
|
||||
_LOGGER.error('No state command specified')
|
||||
return
|
||||
if self._value_template:
|
||||
return CommandSwitch._query_state_value(self._command_state)
|
||||
return CommandSwitch._query_state_code(self._command_state)
|
||||
|
||||
def update(self):
|
||||
""" Update device state. """
|
||||
if self._command_state:
|
||||
payload = str(self._query_state())
|
||||
if self._value_template:
|
||||
payload = template.render_with_possible_json_value(
|
||||
self._hass, self._value_template, payload)
|
||||
self._state = (payload.lower() == "true")
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
""" Turn the device on. """
|
||||
if CommandSwitch._switch(self._command_on):
|
||||
if (CommandSwitch._switch(self._command_on) and
|
||||
not self._command_state):
|
||||
self._state = True
|
||||
self.update_ha_state()
|
||||
self.update_ha_state()
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
""" Turn the device off. """
|
||||
if CommandSwitch._switch(self._command_off):
|
||||
if (CommandSwitch._switch(self._command_off) and
|
||||
not self._command_state):
|
||||
self._state = False
|
||||
self.update_ha_state()
|
||||
self.update_ha_state()
|
||||
|
@ -13,11 +13,13 @@ import homeassistant.util.dt as dt_util
|
||||
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
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,
|
||||
EVENT_HOMEASSISTANT_STOP)
|
||||
|
||||
REQUIREMENTS = ['https://github.com/pavoni/home-assistant-vera-api/archive/'
|
||||
'efdba4e63d58a30bc9b36d9e01e69858af9130b8.zip'
|
||||
'#python-vera==0.1.1']
|
||||
REQUIREMENTS = ['pyvera==0.2.2']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -37,7 +39,16 @@ def get_devices(hass, config):
|
||||
|
||||
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 = []
|
||||
try:
|
||||
devices = vera_controller.get_devices([
|
||||
@ -53,7 +64,8 @@ def get_devices(hass, config):
|
||||
exclude = extra_data.get('exclude', False)
|
||||
|
||||
if exclude is not True:
|
||||
vera_switches.append(VeraSwitch(device, extra_data))
|
||||
vera_switches.append(
|
||||
VeraSwitch(device, vera_controller, extra_data))
|
||||
|
||||
return vera_switches
|
||||
|
||||
@ -66,9 +78,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
class VeraSwitch(ToggleEntity):
|
||||
""" 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.extra_data = extra_data
|
||||
self.controller = controller
|
||||
if self.extra_data and self.extra_data.get('name'):
|
||||
self._name = self.extra_data.get('name')
|
||||
else:
|
||||
@ -77,6 +90,16 @@ class VeraSwitch(ToggleEntity):
|
||||
# for debouncing status check after command is sent
|
||||
self.last_command_send = 0
|
||||
|
||||
self.controller.register(vera_device)
|
||||
self.controller.on(
|
||||
vera_device, self._update_callback)
|
||||
|
||||
def _update_callback(self, _device):
|
||||
""" Called by the vera device callback to update state. """
|
||||
_LOGGER.info(
|
||||
'Subscription update for %s', self.name)
|
||||
self.update_ha_state(True)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
""" Get the mame of the switch. """
|
||||
@ -118,6 +141,11 @@ class VeraSwitch(ToggleEntity):
|
||||
self.vera_device.switch_off()
|
||||
self.is_on_status = False
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
""" Tells Home Assistant not to poll this entity. """
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
""" True if device is on. """
|
||||
|
@ -12,7 +12,7 @@ from homeassistant.components.switch import SwitchDevice
|
||||
from homeassistant.const import (
|
||||
STATE_ON, STATE_OFF, STATE_STANDBY, EVENT_HOMEASSISTANT_STOP)
|
||||
|
||||
REQUIREMENTS = ['pywemo==0.3.4']
|
||||
REQUIREMENTS = ['pywemo==0.3.8']
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_WEMO_SUBSCRIPTION_REGISTRY = None
|
||||
@ -69,15 +69,14 @@ class WemoSwitch(SwitchDevice):
|
||||
def _update_callback(self, _device, _params):
|
||||
""" Called by the wemo device callback to update state. """
|
||||
_LOGGER.info(
|
||||
'Subscription update for %s, sevice=%s',
|
||||
self.name, _device)
|
||||
'Subscription update for %s',
|
||||
_device)
|
||||
self.update_ha_state(True)
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
""" No polling should be needed with subscriptions """
|
||||
# but leave in for initial version in case of issues.
|
||||
return True
|
||||
""" No polling needed with subscriptions """
|
||||
return False
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
|
@ -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
|
||||
https://home-assistant.io/components/thermostat.honeywell/
|
||||
"""
|
||||
import socket
|
||||
import logging
|
||||
import socket
|
||||
|
||||
from homeassistant.components.thermostat import ThermostatDevice
|
||||
from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD, TEMP_CELCIUS)
|
||||
|
||||
@ -15,6 +16,8 @@ REQUIREMENTS = ['evohomeclient==0.2.4']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_AWAY_TEMP = "away_temperature"
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
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)
|
||||
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:
|
||||
_LOGGER.error("Missing required configuration items %s or %s",
|
||||
CONF_USERNAME, CONF_PASSWORD)
|
||||
return False
|
||||
|
||||
evo_api = EvohomeClient(username, password)
|
||||
|
||||
try:
|
||||
zones = evo_api.temperatures(force_refresh=True)
|
||||
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:
|
||||
_LOGGER.error(
|
||||
"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):
|
||||
""" 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._current_temperature = None
|
||||
self._target_temperature = None
|
||||
@ -52,6 +65,8 @@ class RoundThermostat(ThermostatDevice):
|
||||
self._id = zone_id
|
||||
self._master = master
|
||||
self._is_dhw = False
|
||||
self._away_temp = away_temp
|
||||
self._away = False
|
||||
self.update()
|
||||
|
||||
@property
|
||||
@ -80,6 +95,25 @@ class RoundThermostat(ThermostatDevice):
|
||||
""" Set new target 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):
|
||||
try:
|
||||
# Only refresh if this is the "master" device,
|
||||
|
@ -28,7 +28,7 @@ DISCOVER_SWITCHES = 'verisure.switches'
|
||||
DISCOVER_ALARMS = 'verisure.alarm_control_panel'
|
||||
|
||||
DEPENDENCIES = ['alarm_control_panel']
|
||||
REQUIREMENTS = ['vsure==0.4.3']
|
||||
REQUIREMENTS = ['vsure==0.4.5']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -24,6 +24,7 @@ CONF_USERNAME = "username"
|
||||
CONF_PASSWORD = "password"
|
||||
CONF_API_KEY = "api_key"
|
||||
CONF_ACCESS_TOKEN = "access_token"
|
||||
CONF_FILENAME = "filename"
|
||||
|
||||
CONF_VALUE_TEMPLATE = "value_template"
|
||||
|
||||
|
@ -468,6 +468,13 @@ class StateMachine(object):
|
||||
return (entity_id in self._states and
|
||||
self._states[entity_id].state == state)
|
||||
|
||||
def is_state_attr(self, entity_id, name, value):
|
||||
"""Test if entity exists and has a state attribute set to value."""
|
||||
entity_id = entity_id.lower()
|
||||
|
||||
return (entity_id in self._states and
|
||||
self._states[entity_id].attributes.get(name, None) == value)
|
||||
|
||||
def remove(self, entity_id):
|
||||
"""Remove the state of an entity.
|
||||
|
||||
|
@ -36,7 +36,7 @@ def extract_entity_ids(hass, service):
|
||||
service_ent_id = service.data[ATTR_ENTITY_ID]
|
||||
|
||||
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)]
|
||||
|
||||
|
43
homeassistant/helpers/service.py
Normal file
43
homeassistant/helpers/service.py
Normal 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)
|
@ -1,9 +1,6 @@
|
||||
"""
|
||||
homeassistant.helpers.state
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Helpers that help with state related things.
|
||||
"""
|
||||
"""Helpers that help with state related things."""
|
||||
from collections import defaultdict
|
||||
import json
|
||||
import logging
|
||||
|
||||
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
|
||||
is exited.
|
||||
"""
|
||||
|
||||
def __init__(self, hass):
|
||||
"""Initialize a TrackStates block."""
|
||||
self.hass = hass
|
||||
self.states = []
|
||||
|
||||
def __enter__(self):
|
||||
"""Record time from which to track changes."""
|
||||
self.now = dt_util.utcnow()
|
||||
return self.states
|
||||
|
||||
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))
|
||||
|
||||
|
||||
def get_changed_since(states, utc_point_in_time):
|
||||
"""
|
||||
Returns all states that have been changed since utc_point_in_time.
|
||||
"""
|
||||
"""List of states that have been changed since 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]
|
||||
|
||||
|
||||
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):
|
||||
states = [states]
|
||||
|
||||
to_call = defaultdict(list)
|
||||
|
||||
for state in states:
|
||||
current_state = hass.states.get(state.entity_id)
|
||||
|
||||
@ -76,7 +77,18 @@ def reproduce_state(hass, states, blocking=False):
|
||||
state)
|
||||
continue
|
||||
|
||||
service_data = dict(state.attributes)
|
||||
service_data[ATTR_ENTITY_ID] = state.entity_id
|
||||
if state.domain == 'group':
|
||||
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)
|
||||
|
@ -43,7 +43,8 @@ def render(hass, template, variables=None, **kwargs):
|
||||
try:
|
||||
return ENV.from_string(template, {
|
||||
'states': AllStates(hass),
|
||||
'is_state': hass.states.is_state
|
||||
'is_state': hass.states.is_state,
|
||||
'is_state_attr': hass.states.is_state_attr
|
||||
}).render(kwargs).strip()
|
||||
except jinja2.TemplateError as err:
|
||||
raise TemplateError(err)
|
||||
|
@ -16,7 +16,7 @@ fuzzywuzzy==0.8.0
|
||||
pyicloud==0.7.2
|
||||
|
||||
# homeassistant.components.device_tracker.netgear
|
||||
pynetgear==0.3
|
||||
pynetgear==0.3.1
|
||||
|
||||
# homeassistant.components.device_tracker.nmap_tracker
|
||||
python-nmap==0.4.3
|
||||
@ -59,7 +59,7 @@ tellcore-py==1.1.2
|
||||
# homeassistant.components.light.vera
|
||||
# homeassistant.components.sensor.vera
|
||||
# homeassistant.components.switch.vera
|
||||
https://github.com/pavoni/home-assistant-vera-api/archive/efdba4e63d58a30bc9b36d9e01e69858af9130b8.zip#python-vera==0.1.1
|
||||
pyvera==0.2.2
|
||||
|
||||
# homeassistant.components.wink
|
||||
# homeassistant.components.light.wink
|
||||
@ -69,7 +69,7 @@ https://github.com/pavoni/home-assistant-vera-api/archive/efdba4e63d58a30bc9b36d
|
||||
python-wink==0.3.1
|
||||
|
||||
# homeassistant.components.media_player.cast
|
||||
pychromecast==0.6.13
|
||||
pychromecast==0.6.14
|
||||
|
||||
# homeassistant.components.media_player.kodi
|
||||
jsonrpc-requests==0.1
|
||||
@ -135,7 +135,7 @@ eliqonline==1.0.11
|
||||
python-forecastio==1.3.3
|
||||
|
||||
# homeassistant.components.sensor.openweathermap
|
||||
pyowm==2.2.1
|
||||
pyowm==2.3.0
|
||||
|
||||
# homeassistant.components.sensor.rpi_gpio
|
||||
# homeassistant.components.switch.rpi_gpio
|
||||
@ -173,7 +173,7 @@ hikvision==0.4
|
||||
orvibo==1.1.0
|
||||
|
||||
# homeassistant.components.switch.wemo
|
||||
pywemo==0.3.4
|
||||
pywemo==0.3.8
|
||||
|
||||
# homeassistant.components.tellduslive
|
||||
tellive-py==0.5.2
|
||||
@ -191,7 +191,7 @@ python-nest==2.6.0
|
||||
radiotherm==1.2
|
||||
|
||||
# homeassistant.components.verisure
|
||||
vsure==0.4.3
|
||||
vsure==0.4.5
|
||||
|
||||
# homeassistant.components.zwave
|
||||
pydispatcher==2.0.5
|
||||
|
@ -5,6 +5,28 @@
|
||||
|
||||
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
|
||||
NO_LINT=1
|
||||
fi
|
||||
|
14
script/test
14
script/test
@ -5,13 +5,6 @@
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
if [ "$NO_LINT" = "1" ]; then
|
||||
LINT_STATUS=0
|
||||
else
|
||||
script/lint
|
||||
LINT_STATUS=$?
|
||||
fi
|
||||
|
||||
echo "Running tests..."
|
||||
|
||||
if [ "$1" = "coverage" ]; then
|
||||
@ -22,6 +15,13 @@ else
|
||||
TEST_STATUS=$?
|
||||
fi
|
||||
|
||||
if [ "$NO_LINT" = "1" ]; then
|
||||
LINT_STATUS=0
|
||||
else
|
||||
script/lint
|
||||
LINT_STATUS=$?
|
||||
fi
|
||||
|
||||
if [ $LINT_STATUS -eq 0 ]
|
||||
then
|
||||
exit $TEST_STATUS
|
||||
|
@ -162,14 +162,14 @@ class TestAutomationSun(unittest.TestCase):
|
||||
})
|
||||
|
||||
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):
|
||||
self.hass.bus.fire('test_event')
|
||||
self.hass.pool.block_till_done()
|
||||
self.assertEqual(0, len(self.calls))
|
||||
|
||||
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):
|
||||
self.hass.bus.fire('test_event')
|
||||
self.hass.pool.block_till_done()
|
||||
@ -197,14 +197,14 @@ class TestAutomationSun(unittest.TestCase):
|
||||
})
|
||||
|
||||
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):
|
||||
self.hass.bus.fire('test_event')
|
||||
self.hass.pool.block_till_done()
|
||||
self.assertEqual(0, len(self.calls))
|
||||
|
||||
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):
|
||||
self.hass.bus.fire('test_event')
|
||||
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)
|
||||
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(0, len(self.calls))
|
||||
|
||||
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):
|
||||
self.hass.bus.fire('test_event')
|
||||
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)
|
||||
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(0, len(self.calls))
|
||||
|
||||
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):
|
||||
self.hass.bus.fire('test_event')
|
||||
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)
|
||||
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(0, len(self.calls))
|
||||
|
||||
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):
|
||||
self.hass.bus.fire('test_event')
|
||||
self.hass.pool.block_till_done()
|
||||
self.assertEqual(0, len(self.calls))
|
||||
|
||||
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):
|
||||
self.hass.bus.fire('test_event')
|
||||
self.hass.pool.block_till_done()
|
||||
|
205
tests/components/device_tracker/test_locative.py
Normal file
205
tests/components/device_tracker/test_locative.py
Normal file
@ -0,0 +1,205 @@
|
||||
"""
|
||||
tests.components.device_tracker.locative
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Tests the locative device tracker component.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
import requests
|
||||
|
||||
from homeassistant import bootstrap, const
|
||||
import homeassistant.core as ha
|
||||
import homeassistant.components.device_tracker as device_tracker
|
||||
import homeassistant.components.http as http
|
||||
import homeassistant.components.zone as zone
|
||||
|
||||
SERVER_PORT = 8126
|
||||
HTTP_BASE_URL = "http://127.0.0.1:{}".format(SERVER_PORT)
|
||||
|
||||
hass = None
|
||||
|
||||
|
||||
def _url(data={}):
|
||||
""" Helper method to generate urls. """
|
||||
data = "&".join(["{}={}".format(name, value) for name, value in data.items()])
|
||||
return "{}{}locative?{}".format(HTTP_BASE_URL, const.URL_API, data)
|
||||
|
||||
|
||||
@patch('homeassistant.components.http.util.get_local_ip',
|
||||
return_value='127.0.0.1')
|
||||
def setUpModule(mock_get_local_ip): # pylint: disable=invalid-name
|
||||
""" Initalizes a Home Assistant server. """
|
||||
global hass
|
||||
|
||||
hass = ha.HomeAssistant()
|
||||
|
||||
# Set up server
|
||||
bootstrap.setup_component(hass, http.DOMAIN, {
|
||||
http.DOMAIN: {
|
||||
http.CONF_SERVER_PORT: SERVER_PORT
|
||||
}
|
||||
})
|
||||
|
||||
# Set up API
|
||||
bootstrap.setup_component(hass, 'api')
|
||||
|
||||
# Set up device tracker
|
||||
bootstrap.setup_component(hass, device_tracker.DOMAIN, {
|
||||
device_tracker.DOMAIN: {
|
||||
'platform': 'locative'
|
||||
}
|
||||
})
|
||||
|
||||
hass.start()
|
||||
|
||||
|
||||
def tearDownModule(): # pylint: disable=invalid-name
|
||||
""" Stops the Home Assistant server. """
|
||||
hass.stop()
|
||||
|
||||
# Stub out update_config or else Travis CI raises an exception
|
||||
@patch('homeassistant.components.device_tracker.update_config')
|
||||
class TestLocative(unittest.TestCase):
|
||||
""" Test Locative """
|
||||
|
||||
def test_missing_data(self, update_config):
|
||||
data = {
|
||||
'latitude': 1.0,
|
||||
'longitude': 1.1,
|
||||
'device': '123',
|
||||
'id': 'Home',
|
||||
'trigger': 'enter'
|
||||
}
|
||||
|
||||
# No data
|
||||
req = requests.get(_url({}))
|
||||
self.assertEqual(422, req.status_code)
|
||||
|
||||
# No latitude
|
||||
copy = data.copy()
|
||||
del copy['latitude']
|
||||
req = requests.get(_url(copy))
|
||||
self.assertEqual(422, req.status_code)
|
||||
|
||||
# No device
|
||||
copy = data.copy()
|
||||
del copy['device']
|
||||
req = requests.get(_url(copy))
|
||||
self.assertEqual(422, req.status_code)
|
||||
|
||||
# No location
|
||||
copy = data.copy()
|
||||
del copy['id']
|
||||
req = requests.get(_url(copy))
|
||||
self.assertEqual(422, req.status_code)
|
||||
|
||||
# No trigger
|
||||
copy = data.copy()
|
||||
del copy['trigger']
|
||||
req = requests.get(_url(copy))
|
||||
self.assertEqual(422, req.status_code)
|
||||
|
||||
# Test message
|
||||
copy = data.copy()
|
||||
copy['trigger'] = 'test'
|
||||
req = requests.get(_url(copy))
|
||||
self.assertEqual(200, req.status_code)
|
||||
|
||||
# Unknown trigger
|
||||
copy = data.copy()
|
||||
copy['trigger'] = 'foobar'
|
||||
req = requests.get(_url(copy))
|
||||
self.assertEqual(422, req.status_code)
|
||||
|
||||
|
||||
def test_enter_and_exit(self, update_config):
|
||||
""" Test when there is a known zone """
|
||||
data = {
|
||||
'latitude': 40.7855,
|
||||
'longitude': -111.7367,
|
||||
'device': '123',
|
||||
'id': 'Home',
|
||||
'trigger': 'enter'
|
||||
}
|
||||
|
||||
# Enter the Home
|
||||
req = requests.get(_url(data))
|
||||
self.assertEqual(200, req.status_code)
|
||||
state_name = hass.states.get('{}.{}'.format('device_tracker', data['device'])).state
|
||||
self.assertEqual(state_name, 'home')
|
||||
|
||||
data['id'] = 'HOME'
|
||||
data['trigger'] = 'exit'
|
||||
|
||||
# Exit Home
|
||||
req = requests.get(_url(data))
|
||||
self.assertEqual(200, req.status_code)
|
||||
state_name = hass.states.get('{}.{}'.format('device_tracker', data['device'])).state
|
||||
self.assertEqual(state_name, 'not_home')
|
||||
|
||||
data['id'] = 'hOmE'
|
||||
data['trigger'] = 'enter'
|
||||
|
||||
# Enter Home again
|
||||
req = requests.get(_url(data))
|
||||
self.assertEqual(200, req.status_code)
|
||||
state_name = hass.states.get('{}.{}'.format('device_tracker', data['device'])).state
|
||||
self.assertEqual(state_name, 'home')
|
||||
|
||||
data['trigger'] = 'exit'
|
||||
|
||||
# Exit Home
|
||||
req = requests.get(_url(data))
|
||||
self.assertEqual(200, req.status_code)
|
||||
state_name = hass.states.get('{}.{}'.format('device_tracker', data['device'])).state
|
||||
self.assertEqual(state_name, 'not_home')
|
||||
|
||||
data['id'] = 'work'
|
||||
data['trigger'] = 'enter'
|
||||
|
||||
# Enter Work
|
||||
req = requests.get(_url(data))
|
||||
self.assertEqual(200, req.status_code)
|
||||
state_name = hass.states.get('{}.{}'.format('device_tracker', data['device'])).state
|
||||
self.assertEqual(state_name, 'work')
|
||||
|
||||
|
||||
def test_exit_after_enter(self, update_config):
|
||||
""" Test when an exit message comes after an enter message """
|
||||
|
||||
data = {
|
||||
'latitude': 40.7855,
|
||||
'longitude': -111.7367,
|
||||
'device': '123',
|
||||
'id': 'Home',
|
||||
'trigger': 'enter'
|
||||
}
|
||||
|
||||
# Enter Home
|
||||
req = requests.get(_url(data))
|
||||
self.assertEqual(200, req.status_code)
|
||||
|
||||
state = hass.states.get('{}.{}'.format('device_tracker', data['device']))
|
||||
self.assertEqual(state.state, 'home')
|
||||
|
||||
data['id'] = 'Work'
|
||||
|
||||
# Enter Work
|
||||
req = requests.get(_url(data))
|
||||
self.assertEqual(200, req.status_code)
|
||||
|
||||
state = hass.states.get('{}.{}'.format('device_tracker', data['device']))
|
||||
self.assertEqual(state.state, 'work')
|
||||
|
||||
data['id'] = 'Home'
|
||||
data['trigger'] = 'exit'
|
||||
|
||||
# Exit Home
|
||||
req = requests.get(_url(data))
|
||||
self.assertEqual(200, req.status_code)
|
||||
|
||||
state = hass.states.get('{}.{}'.format('device_tracker', data['device']))
|
||||
self.assertEqual(state.state, 'work')
|
@ -4,12 +4,14 @@ tests.components.sensor.test_yr
|
||||
|
||||
Tests Yr sensor.
|
||||
"""
|
||||
from datetime import datetime
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
import homeassistant.core as ha
|
||||
import homeassistant.components.sensor as sensor
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
|
||||
@pytest.mark.usefixtures('betamax_session')
|
||||
@ -26,14 +28,18 @@ class TestSensorYr:
|
||||
self.hass.stop()
|
||||
|
||||
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',
|
||||
return_value=betamax_session):
|
||||
assert sensor.setup(self.hass, {
|
||||
'sensor': {
|
||||
'platform': 'yr',
|
||||
'elevation': 0,
|
||||
}
|
||||
})
|
||||
with patch('homeassistant.components.sensor.yr.dt_util.utcnow',
|
||||
return_value=now):
|
||||
assert sensor.setup(self.hass, {
|
||||
'sensor': {
|
||||
'platform': 'yr',
|
||||
'elevation': 0,
|
||||
}
|
||||
})
|
||||
|
||||
state = self.hass.states.get('sensor.yr_symbol')
|
||||
|
||||
|
158
tests/components/switch/test_command_switch.py
Normal file
158
tests/components/switch/test_command_switch.py
Normal file
@ -0,0 +1,158 @@
|
||||
"""
|
||||
tests.components.switch.test_command_switch
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Tests command switch.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from homeassistant import core
|
||||
from homeassistant.const import STATE_ON, STATE_OFF
|
||||
import homeassistant.components.switch as switch
|
||||
|
||||
|
||||
class TestCommandSwitch(unittest.TestCase):
|
||||
""" Test the command switch. """
|
||||
|
||||
def setUp(self): # pylint: disable=invalid-name
|
||||
self.hass = core.HomeAssistant()
|
||||
|
||||
def tearDown(self): # pylint: disable=invalid-name
|
||||
""" Stop down stuff we started. """
|
||||
self.hass.stop()
|
||||
|
||||
def test_state_none(self):
|
||||
with tempfile.TemporaryDirectory() as tempdirname:
|
||||
path = os.path.join(tempdirname, 'switch_status')
|
||||
test_switch = {
|
||||
'oncmd': 'echo 1 > {}'.format(path),
|
||||
'offcmd': 'echo 0 > {}'.format(path),
|
||||
}
|
||||
self.assertTrue(switch.setup(self.hass, {
|
||||
'switch': {
|
||||
'platform': 'command_switch',
|
||||
'switches': {
|
||||
'test': test_switch
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
state = self.hass.states.get('switch.test')
|
||||
self.assertEqual(STATE_OFF, state.state)
|
||||
|
||||
switch.turn_on(self.hass, 'switch.test')
|
||||
self.hass.pool.block_till_done()
|
||||
|
||||
state = self.hass.states.get('switch.test')
|
||||
self.assertEqual(STATE_ON, state.state)
|
||||
|
||||
switch.turn_off(self.hass, 'switch.test')
|
||||
self.hass.pool.block_till_done()
|
||||
|
||||
state = self.hass.states.get('switch.test')
|
||||
self.assertEqual(STATE_OFF, state.state)
|
||||
|
||||
|
||||
def test_state_value(self):
|
||||
with tempfile.TemporaryDirectory() as tempdirname:
|
||||
path = os.path.join(tempdirname, 'switch_status')
|
||||
test_switch = {
|
||||
'statecmd': 'cat {}'.format(path),
|
||||
'oncmd': 'echo 1 > {}'.format(path),
|
||||
'offcmd': 'echo 0 > {}'.format(path),
|
||||
'value_template': '{{ value=="1" }}'
|
||||
}
|
||||
self.assertTrue(switch.setup(self.hass, {
|
||||
'switch': {
|
||||
'platform': 'command_switch',
|
||||
'switches': {
|
||||
'test': test_switch
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
state = self.hass.states.get('switch.test')
|
||||
self.assertEqual(STATE_OFF, state.state)
|
||||
|
||||
switch.turn_on(self.hass, 'switch.test')
|
||||
self.hass.pool.block_till_done()
|
||||
|
||||
state = self.hass.states.get('switch.test')
|
||||
self.assertEqual(STATE_ON, state.state)
|
||||
|
||||
switch.turn_off(self.hass, 'switch.test')
|
||||
self.hass.pool.block_till_done()
|
||||
|
||||
state = self.hass.states.get('switch.test')
|
||||
self.assertEqual(STATE_OFF, state.state)
|
||||
|
||||
|
||||
def test_state_json_value(self):
|
||||
with tempfile.TemporaryDirectory() as tempdirname:
|
||||
path = os.path.join(tempdirname, 'switch_status')
|
||||
oncmd = json.dumps({'status': 'ok'})
|
||||
offcmd = json.dumps({'status': 'nope'})
|
||||
test_switch = {
|
||||
'statecmd': 'cat {}'.format(path),
|
||||
'oncmd': 'echo \'{}\' > {}'.format(oncmd, path),
|
||||
'offcmd': 'echo \'{}\' > {}'.format(offcmd, path),
|
||||
'value_template': '{{ value_json.status=="ok" }}'
|
||||
}
|
||||
self.assertTrue(switch.setup(self.hass, {
|
||||
'switch': {
|
||||
'platform': 'command_switch',
|
||||
'switches': {
|
||||
'test': test_switch
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
state = self.hass.states.get('switch.test')
|
||||
self.assertEqual(STATE_OFF, state.state)
|
||||
|
||||
switch.turn_on(self.hass, 'switch.test')
|
||||
self.hass.pool.block_till_done()
|
||||
|
||||
state = self.hass.states.get('switch.test')
|
||||
self.assertEqual(STATE_ON, state.state)
|
||||
|
||||
switch.turn_off(self.hass, 'switch.test')
|
||||
self.hass.pool.block_till_done()
|
||||
|
||||
state = self.hass.states.get('switch.test')
|
||||
self.assertEqual(STATE_OFF, state.state)
|
||||
|
||||
def test_state_code(self):
|
||||
with tempfile.TemporaryDirectory() as tempdirname:
|
||||
path = os.path.join(tempdirname, 'switch_status')
|
||||
test_switch = {
|
||||
'statecmd': 'cat {}'.format(path),
|
||||
'oncmd': 'echo 1 > {}'.format(path),
|
||||
'offcmd': 'echo 0 > {}'.format(path),
|
||||
}
|
||||
self.assertTrue(switch.setup(self.hass, {
|
||||
'switch': {
|
||||
'platform': 'command_switch',
|
||||
'switches': {
|
||||
'test': test_switch
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
state = self.hass.states.get('switch.test')
|
||||
self.assertEqual(STATE_OFF, state.state)
|
||||
|
||||
switch.turn_on(self.hass, 'switch.test')
|
||||
self.hass.pool.block_till_done()
|
||||
|
||||
state = self.hass.states.get('switch.test')
|
||||
self.assertEqual(STATE_ON, state.state)
|
||||
|
||||
switch.turn_off(self.hass, 'switch.test')
|
||||
self.hass.pool.block_till_done()
|
||||
|
||||
state = self.hass.states.get('switch.test')
|
||||
self.assertEqual(STATE_ON, state.state)
|
@ -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}
|
||||
|
||||
hass = None
|
||||
calls = []
|
||||
|
||||
|
||||
@patch('homeassistant.components.http.util.get_local_ip',
|
||||
return_value='127.0.0.1')
|
||||
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
|
||||
|
||||
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.CONF_SERVER_PORT: SERVER_PORT}})
|
||||
|
||||
hass.services.register('test', 'alexa', lambda call: calls.append(call))
|
||||
|
||||
bootstrap.setup_component(hass, alexa.DOMAIN, {
|
||||
'alexa': {
|
||||
'intents': {
|
||||
@ -61,7 +64,20 @@ def setUpModule(mock_get_local_ip): # pylint: disable=invalid-name
|
||||
'GetZodiacHoroscopeIntent': {
|
||||
'speech': {
|
||||
'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')
|
||||
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):
|
||||
data = {
|
||||
'version': '1.0',
|
||||
|
@ -6,20 +6,22 @@ Tests core compoments.
|
||||
"""
|
||||
# pylint: disable=protected-access,too-many-public-methods
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
import homeassistant.core as ha
|
||||
import homeassistant.loader as loader
|
||||
from homeassistant.const import (
|
||||
STATE_ON, STATE_OFF, SERVICE_TURN_ON, SERVICE_TURN_OFF)
|
||||
import homeassistant.components as comps
|
||||
|
||||
from tests.common import get_test_home_assistant
|
||||
|
||||
|
||||
class TestComponentsCore(unittest.TestCase):
|
||||
""" Tests homeassistant.components module. """
|
||||
|
||||
def setUp(self): # pylint: disable=invalid-name
|
||||
""" Init needed objects. """
|
||||
self.hass = ha.HomeAssistant()
|
||||
self.hass = get_test_home_assistant()
|
||||
self.assertTrue(comps.setup(self.hass, {}))
|
||||
|
||||
self.hass.states.set('light.Bowl', STATE_ON)
|
||||
@ -58,3 +60,24 @@ class TestComponentsCore(unittest.TestCase):
|
||||
self.hass.pool.block_till_done()
|
||||
|
||||
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])
|
||||
|
68
tests/helpers/test_service.py
Normal file
68
tests/helpers/test_service.py
Normal 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
148
tests/helpers/test_state.py
Normal 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'))
|
@ -321,6 +321,18 @@ class TestStateMachine(unittest.TestCase):
|
||||
self.assertFalse(self.states.is_state('light.Bowl', 'off'))
|
||||
self.assertFalse(self.states.is_state('light.Non_existing', 'on'))
|
||||
|
||||
def test_is_state_attr(self):
|
||||
""" Test is_state_attr method. """
|
||||
self.states.set("light.Bowl", "on", {"brightness": 100})
|
||||
self.assertTrue(
|
||||
self.states.is_state_attr('light.Bowl', 'brightness', 100))
|
||||
self.assertFalse(
|
||||
self.states.is_state_attr('light.Bowl', 'friendly_name', 200))
|
||||
self.assertFalse(
|
||||
self.states.is_state_attr('light.Bowl', 'friendly_name', 'Bowl'))
|
||||
self.assertFalse(
|
||||
self.states.is_state_attr('light.Non_existing', 'brightness', 100))
|
||||
|
||||
def test_entity_ids(self):
|
||||
""" Test get_entity_ids method. """
|
||||
ent_ids = self.states.entity_ids()
|
||||
|
@ -117,6 +117,14 @@ class TestUtilTemplate(unittest.TestCase):
|
||||
self.hass,
|
||||
'{% if is_state("test.object", "available") %}yes{% else %}no{% endif %}'))
|
||||
|
||||
def test_is_state_attr(self):
|
||||
self.hass.states.set('test.object', 'available', {'mode': 'on'})
|
||||
self.assertEqual(
|
||||
'yes',
|
||||
template.render(
|
||||
self.hass,
|
||||
'{% if is_state_attr("test.object", "mode", "on") %}yes{% else %}no{% endif %}'))
|
||||
|
||||
def test_states_function(self):
|
||||
self.hass.states.set('test.object', 'available')
|
||||
self.assertEqual(
|
||||
|
Loading…
x
Reference in New Issue
Block a user