Merge remote-tracking branch 'refs/remotes/home-assistant/dev' into Homematic_fix

This commit is contained in:
Pascal Vizeli 2016-06-30 23:44:27 +02:00
commit d0b1619946
74 changed files with 2705 additions and 1219 deletions

View File

@ -3,7 +3,6 @@
import logging import logging
import logging.handlers import logging.handlers
import os import os
import shutil
import sys import sys
from collections import defaultdict from collections import defaultdict
from threading import RLock from threading import RLock
@ -12,21 +11,15 @@ import voluptuous as vol
import homeassistant.components as core_components import homeassistant.components as core_components
from homeassistant.components import group, persistent_notification from homeassistant.components import group, persistent_notification
import homeassistant.config as config_util import homeassistant.config as conf_util
import homeassistant.core as core import homeassistant.core as core
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
import homeassistant.loader as loader import homeassistant.loader as loader
import homeassistant.util.dt as date_util
import homeassistant.util.location as loc_util
import homeassistant.util.package as pkg_util import homeassistant.util.package as pkg_util
from homeassistant.const import ( from homeassistant.const import EVENT_COMPONENT_LOADED, PLATFORM_FORMAT
CONF_CUSTOMIZE, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME,
CONF_TEMPERATURE_UNIT, CONF_TIME_ZONE, EVENT_COMPONENT_LOADED,
TEMP_CELSIUS, TEMP_FAHRENHEIT, PLATFORM_FORMAT, __version__)
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import ( from homeassistant.helpers import (
event_decorators, service, config_per_platform, extract_domain_configs, event_decorators, service, config_per_platform, extract_domain_configs)
entity)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
_SETUP_LOCK = RLock() _SETUP_LOCK = RLock()
@ -208,11 +201,6 @@ def prepare_setup_platform(hass, config, domain, platform_name):
return platform return platform
def mount_local_lib_path(config_dir):
"""Add local library to Python Path."""
sys.path.insert(0, os.path.join(config_dir, 'deps'))
# pylint: disable=too-many-branches, too-many-statements, too-many-arguments # pylint: disable=too-many-branches, too-many-statements, too-many-arguments
def from_config_dict(config, hass=None, config_dir=None, enable_log=True, def from_config_dict(config, hass=None, config_dir=None, enable_log=True,
verbose=False, skip_pip=False, verbose=False, skip_pip=False,
@ -226,18 +214,17 @@ def from_config_dict(config, hass=None, config_dir=None, enable_log=True,
if config_dir is not None: if config_dir is not None:
config_dir = os.path.abspath(config_dir) config_dir = os.path.abspath(config_dir)
hass.config.config_dir = config_dir hass.config.config_dir = config_dir
mount_local_lib_path(config_dir) _mount_local_lib_path(config_dir)
core_config = config.get(core.DOMAIN, {}) core_config = config.get(core.DOMAIN, {})
try: try:
process_ha_core_config(hass, config_util.CORE_CONFIG_SCHEMA( conf_util.process_ha_core_config(hass, core_config)
core_config)) except vol.Invalid as ex:
except vol.MultipleInvalid as ex:
cv.log_exception(_LOGGER, ex, 'homeassistant', core_config) cv.log_exception(_LOGGER, ex, 'homeassistant', core_config)
return None return None
process_ha_config_upgrade(hass) conf_util.process_ha_config_upgrade(hass)
if enable_log: if enable_log:
enable_logging(hass, verbose, log_rotate_days) enable_logging(hass, verbose, log_rotate_days)
@ -292,12 +279,12 @@ def from_config_file(config_path, hass=None, verbose=False, skip_pip=True,
# Set config dir to directory holding config file # Set config dir to directory holding config file
config_dir = os.path.abspath(os.path.dirname(config_path)) config_dir = os.path.abspath(os.path.dirname(config_path))
hass.config.config_dir = config_dir hass.config.config_dir = config_dir
mount_local_lib_path(config_dir) _mount_local_lib_path(config_dir)
enable_logging(hass, verbose, log_rotate_days) enable_logging(hass, verbose, log_rotate_days)
try: try:
config_dict = config_util.load_yaml_config_file(config_path) config_dict = conf_util.load_yaml_config_file(config_path)
except HomeAssistantError: except HomeAssistantError:
return None return None
@ -356,100 +343,12 @@ def enable_logging(hass, verbose=False, log_rotate_days=None):
'Unable to setup error log %s (access denied)', err_log_path) 'Unable to setup error log %s (access denied)', err_log_path)
def process_ha_config_upgrade(hass):
"""Upgrade config if necessary."""
version_path = hass.config.path('.HA_VERSION')
try:
with open(version_path, 'rt') as inp:
conf_version = inp.readline().strip()
except FileNotFoundError:
# Last version to not have this file
conf_version = '0.7.7'
if conf_version == __version__:
return
_LOGGER.info('Upgrading config directory from %s to %s', conf_version,
__version__)
# This was where dependencies were installed before v0.18
# Probably should keep this around until ~v0.20.
lib_path = hass.config.path('lib')
if os.path.isdir(lib_path):
shutil.rmtree(lib_path)
lib_path = hass.config.path('deps')
if os.path.isdir(lib_path):
shutil.rmtree(lib_path)
with open(version_path, 'wt') as outp:
outp.write(__version__)
def process_ha_core_config(hass, config):
"""Process the [homeassistant] section from the config."""
hac = hass.config
def set_time_zone(time_zone_str):
"""Helper method to set time zone."""
if time_zone_str is None:
return
time_zone = date_util.get_time_zone(time_zone_str)
if time_zone:
hac.time_zone = time_zone
date_util.set_default_time_zone(time_zone)
else:
_LOGGER.error('Received invalid time zone %s', time_zone_str)
for key, attr in ((CONF_LATITUDE, 'latitude'),
(CONF_LONGITUDE, 'longitude'),
(CONF_NAME, 'location_name')):
if key in config:
setattr(hac, attr, config[key])
if CONF_TIME_ZONE in config:
set_time_zone(config.get(CONF_TIME_ZONE))
entity.set_customize(config.get(CONF_CUSTOMIZE))
if CONF_TEMPERATURE_UNIT in config:
hac.temperature_unit = config[CONF_TEMPERATURE_UNIT]
# If we miss some of the needed values, auto detect them
if None not in (
hac.latitude, hac.longitude, hac.temperature_unit, hac.time_zone):
return
_LOGGER.warning('Incomplete core config. Auto detecting location and '
'temperature unit')
info = loc_util.detect_location_info()
if info is None:
_LOGGER.error('Could not detect location information')
return
if hac.latitude is None and hac.longitude is None:
hac.latitude = info.latitude
hac.longitude = info.longitude
if hac.temperature_unit is None:
if info.use_fahrenheit:
hac.temperature_unit = TEMP_FAHRENHEIT
else:
hac.temperature_unit = TEMP_CELSIUS
if hac.location_name is None:
hac.location_name = info.city
if hac.time_zone is None:
set_time_zone(info.time_zone)
def _ensure_loader_prepared(hass): def _ensure_loader_prepared(hass):
"""Ensure Home Assistant loader is prepared.""" """Ensure Home Assistant loader is prepared."""
if not loader.PREPARED: if not loader.PREPARED:
loader.prepare(hass) loader.prepare(hass)
def _mount_local_lib_path(config_dir):
"""Add local library to Python Path."""
sys.path.insert(0, os.path.join(config_dir, 'deps'))

View File

@ -121,16 +121,16 @@ def setup(hass, config):
def handle_reload_config(call): def handle_reload_config(call):
"""Service handler for reloading core config.""" """Service handler for reloading core config."""
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant import config, bootstrap from homeassistant import config as conf_util
try: try:
path = config.find_config_file(hass.config.config_dir) path = conf_util.find_config_file(hass.config.config_dir)
conf = config.load_yaml_config_file(path) conf = conf_util.load_yaml_config_file(path)
except HomeAssistantError as err: except HomeAssistantError as err:
_LOGGER.error(err) _LOGGER.error(err)
return return
bootstrap.process_ha_core_config(hass, conf.get(ha.DOMAIN) or {}) conf_util.process_ha_core_config(hass, conf.get(ha.DOMAIN) or {})
hass.services.register(ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG, hass.services.register(ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG,
handle_reload_config) handle_reload_config)

View File

@ -6,7 +6,7 @@ https://home-assistant.io/developers/api/
""" """
import json import json
import logging import logging
from time import time import queue
import homeassistant.core as ha import homeassistant.core as ha
import homeassistant.remote as rem import homeassistant.remote as rem
@ -72,19 +72,14 @@ class APIEventStream(HomeAssistantView):
def get(self, request): def get(self, request):
"""Provide a streaming interface for the event bus.""" """Provide a streaming interface for the event bus."""
from eventlet.queue import LightQueue, Empty
import eventlet
cur_hub = eventlet.hubs.get_hub()
request.environ['eventlet.minimum_write_chunk_size'] = 0
to_write = LightQueue()
stop_obj = object() stop_obj = object()
to_write = queue.Queue()
restrict = request.args.get('restrict') restrict = request.args.get('restrict')
if restrict: if restrict:
restrict = restrict.split(',') restrict = restrict.split(',') + [EVENT_HOMEASSISTANT_STOP]
def thread_forward_events(event): def forward_events(event):
"""Forward events to the open request.""" """Forward events to the open request."""
if event.event_type == EVENT_TIME_CHANGED: if event.event_type == EVENT_TIME_CHANGED:
return return
@ -99,28 +94,20 @@ class APIEventStream(HomeAssistantView):
else: else:
data = json.dumps(event, cls=rem.JSONEncoder) data = json.dumps(event, cls=rem.JSONEncoder)
cur_hub.schedule_call_global(0, lambda: to_write.put(data)) to_write.put(data)
def stream(): def stream():
"""Stream events to response.""" """Stream events to response."""
self.hass.bus.listen(MATCH_ALL, thread_forward_events) self.hass.bus.listen(MATCH_ALL, forward_events)
_LOGGER.debug('STREAM %s ATTACHED', id(stop_obj)) _LOGGER.debug('STREAM %s ATTACHED', id(stop_obj))
last_msg = time()
# Fire off one message right away to have browsers fire open event # Fire off one message right away to have browsers fire open event
to_write.put(STREAM_PING_PAYLOAD) to_write.put(STREAM_PING_PAYLOAD)
while True: while True:
try: try:
# Somehow our queue.get sometimes takes too long to payload = to_write.get(timeout=STREAM_PING_INTERVAL)
# be notified of arrival of data. Probably
# because of our spawning on hub in other thread
# hack. Because current goal is to get this out,
# We just timeout every second because it will
# return right away if qsize() > 0.
# So yes, we're basically polling :(
payload = to_write.get(timeout=1)
if payload is stop_obj: if payload is stop_obj:
break break
@ -129,15 +116,13 @@ class APIEventStream(HomeAssistantView):
_LOGGER.debug('STREAM %s WRITING %s', id(stop_obj), _LOGGER.debug('STREAM %s WRITING %s', id(stop_obj),
msg.strip()) msg.strip())
yield msg.encode("UTF-8") yield msg.encode("UTF-8")
last_msg = time() except queue.Empty:
except Empty: to_write.put(STREAM_PING_PAYLOAD)
if time() - last_msg > 50:
to_write.put(STREAM_PING_PAYLOAD)
except GeneratorExit: except GeneratorExit:
_LOGGER.debug('STREAM %s RESPONSE CLOSED', id(stop_obj))
break break
self.hass.bus.remove_listener(MATCH_ALL, thread_forward_events) _LOGGER.debug('STREAM %s RESPONSE CLOSED', id(stop_obj))
self.hass.bus.remove_listener(MATCH_ALL, forward_events)
return self.Response(stream(), mimetype='text/event-stream') return self.Response(stream(), mimetype='text/event-stream')

View File

@ -1,33 +1,9 @@
""" """
The homematic binary sensor platform. Support for Homematic binary sensors.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/binary_sensor.homematic/ https://home-assistant.io/components/binary_sensor.homematic/
Important: For this platform to work the homematic component has to be
properly configured.
Configuration (single channel, simple device):
binary_sensor:
- platform: homematic
address: "<Homematic address for device>" # e.g. "JEQ0XXXXXXX"
name: "<User defined name>" (optional)
Configuration (multiple channels, like motion detector with buttons):
binary_sensor:
- platform: homematic
address: "<Homematic address for device>" # e.g. "JEQ0XXXXXXX"
param: <MOTION|PRESS_SHORT...> (device-dependent) (optional)
button: n (integer of channel to map, device-dependent) (optional)
name: "<User defined name>" (optional)
binary_sensor:
- platform: homematic
...
""" """
import logging import logging
from homeassistant.const import STATE_UNKNOWN from homeassistant.const import STATE_UNKNOWN
from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.binary_sensor import BinarySensorDevice
@ -47,44 +23,25 @@ SENSOR_TYPES_CLASS = {
"RemoteMotion": None "RemoteMotion": None
} }
SUPPORT_HM_EVENT_AS_BINMOD = [
"PRESS_LONG",
"PRESS_SHORT"
]
def setup_platform(hass, config, add_callback_devices, discovery_info=None): def setup_platform(hass, config, add_callback_devices, discovery_info=None):
"""Setup the platform.""" """Setup the Homematic binary sensor platform."""
if discovery_info: if discovery_info is None:
return homematic.setup_hmdevice_discovery_helper(HMBinarySensor, return
discovery_info,
add_callback_devices) return homematic.setup_hmdevice_discovery_helper(HMBinarySensor,
# Manual discovery_info,
return homematic.setup_hmdevice_entity_helper(HMBinarySensor, add_callback_devices)
config,
add_callback_devices)
class HMBinarySensor(homematic.HMDevice, BinarySensorDevice): class HMBinarySensor(homematic.HMDevice, BinarySensorDevice):
"""Represents diverse binary Homematic units in Home Assistant.""" """Representation of a binary Homematic device."""
@property @property
def is_on(self): def is_on(self):
"""Return True if switch is on.""" """Return true if switch is on."""
if not self.available: if not self.available:
return False return False
# no binary is defined, check all!
if self._state is None:
available_bin = self._create_binary_list_from_hm()
for binary in available_bin:
try:
if binary in self._data and self._data[binary] == 1:
return True
except (ValueError, TypeError):
_LOGGER.warning("%s datatype error!", self._name)
return False
# single binary
return bool(self._hm_get_state()) return bool(self._hm_get_state())
@property @property
@ -107,63 +64,37 @@ class HMBinarySensor(homematic.HMDevice, BinarySensorDevice):
if not super()._check_hm_to_ha_object(): if not super()._check_hm_to_ha_object():
return False return False
# check if the homematic device correct for this HA device # check if the Homematic device correct for this HA device
if not isinstance(self._hmdevice, pyHMBinarySensor): if not isinstance(self._hmdevice, pyHMBinarySensor):
_LOGGER.critical("This %s can't be use as binary!", self._name) _LOGGER.critical("This %s can't be use as binary", self._name)
return False return False
# load possible binary sensor
available_bin = self._create_binary_list_from_hm()
# if exists user value? # if exists user value?
if self._state and self._state not in available_bin: if self._state and self._state not in self._hmdevice.BINARYNODE:
_LOGGER.critical("This %s have no binary with %s!", self._name, _LOGGER.critical("This %s have no binary with %s", self._name,
self._state) self._state)
return False return False
# only check and give a warining to User # only check and give a warning to the user
if self._state is None and len(available_bin) > 1: if self._state is None and len(self._hmdevice.BINARYNODE) > 1:
_LOGGER.warning("%s have multible binary params. It use all " + _LOGGER.critical("%s have multiple binary params. It use all "
"binary nodes as one. Possible param values: %s", "binary nodes as one. Possible param values: %s",
self._name, str(available_bin)) self._name, str(self._hmdevice.BINARYNODE))
return False
return True return True
def _init_data_struct(self): def _init_data_struct(self):
"""Generate a data struct (self._data) from hm metadata.""" """Generate a data struct (self._data) from the Homematic metadata."""
super()._init_data_struct() super()._init_data_struct()
# load possible binary sensor
available_bin = self._create_binary_list_from_hm()
# object have 1 binary # object have 1 binary
if self._state is None and len(available_bin) == 1: if self._state is None and len(self._hmdevice.BINARYNODE) == 1:
for value in available_bin: for value in self._hmdevice.BINARYNODE:
self._state = value self._state = value
# no binary is definit, use all binary for state
if self._state is None and len(available_bin) > 1:
for node in available_bin:
self._data.update({node: STATE_UNKNOWN})
# add state to data struct # add state to data struct
if self._state: if self._state:
_LOGGER.debug("%s init datastruct with main node '%s'", self._name, _LOGGER.debug("%s init datastruct with main node '%s'", self._name,
self._state) self._state)
self._data.update({self._state: STATE_UNKNOWN}) self._data.update({self._state: STATE_UNKNOWN})
def _create_binary_list_from_hm(self):
"""Generate a own metadata for binary_sensors."""
bin_data = {}
if not self._hmdevice:
return bin_data
# copy all data from BINARYNODE
bin_data.update(self._hmdevice.BINARYNODE)
# copy all hm event they are supportet by this object
for event, channel in self._hmdevice.EVENTNODE.items():
if event in SUPPORT_HM_EVENT_AS_BINMOD:
bin_data.update({event: channel})
return bin_data

View File

@ -7,10 +7,12 @@ at https://home-assistant.io/components/sensor.wink/
import logging import logging
from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.const import CONF_ACCESS_TOKEN, ATTR_BATTERY_LEVEL from homeassistant.components.sensor.wink import WinkDevice
from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.loader import get_component
REQUIREMENTS = ['python-wink==0.7.7'] REQUIREMENTS = ['python-wink==0.7.8', 'pubnub==3.7.8']
# These are the available sensors mapped to binary_sensor class # These are the available sensors mapped to binary_sensor class
SENSOR_TYPES = { SENSOR_TYPES = {
@ -41,14 +43,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
add_devices([WinkBinarySensorDevice(sensor)]) add_devices([WinkBinarySensorDevice(sensor)])
class WinkBinarySensorDevice(BinarySensorDevice, Entity): class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity):
"""Representation of a Wink sensor.""" """Representation of a Wink sensor."""
def __init__(self, wink): def __init__(self, wink):
"""Initialize the Wink binary sensor.""" """Initialize the Wink binary sensor."""
self.wink = wink super().__init__(wink)
wink = get_component('wink')
self._unit_of_measurement = self.wink.UNIT self._unit_of_measurement = self.wink.UNIT
self._battery = self.wink.battery_level
self.capability = self.wink.capability() self.capability = self.wink.capability()
@property @property
@ -67,35 +69,3 @@ class WinkBinarySensorDevice(BinarySensorDevice, Entity):
def sensor_class(self): def sensor_class(self):
"""Return the class of this sensor, from SENSOR_CLASSES.""" """Return the class of this sensor, from SENSOR_CLASSES."""
return SENSOR_TYPES.get(self.capability) return SENSOR_TYPES.get(self.capability)
@property
def unique_id(self):
"""Return the ID of this wink sensor."""
return "{}.{}".format(self.__class__, self.wink.device_id())
@property
def name(self):
"""Return the name of the sensor if any."""
return self.wink.name()
@property
def available(self):
"""True if connection == True."""
return self.wink.available
def update(self):
"""Update state of the sensor."""
self.wink.update_state()
@property
def device_state_attributes(self):
"""Return the state attributes."""
if self._battery:
return {
ATTR_BATTERY_LEVEL: self._battery_level,
}
@property
def _battery_level(self):
"""Return the battery level."""
return self.wink.battery_level * 100

View File

@ -6,6 +6,7 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/components/camera/ https://home-assistant.io/components/camera/
""" """
import logging import logging
import time
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
@ -81,8 +82,6 @@ class Camera(Entity):
def mjpeg_stream(self, response): def mjpeg_stream(self, response):
"""Generate an HTTP MJPEG stream from camera images.""" """Generate an HTTP MJPEG stream from camera images."""
import eventlet
def stream(): def stream():
"""Stream images as mjpeg stream.""" """Stream images as mjpeg stream."""
try: try:
@ -99,7 +98,7 @@ class Camera(Entity):
last_image = img_bytes last_image = img_bytes
eventlet.sleep(0.5) time.sleep(0.5)
except GeneratorExit: except GeneratorExit:
pass pass

View File

@ -1,5 +1,9 @@
"""Camera platform that has a Raspberry Pi camera.""" """
Camera platform that has a Raspberry Pi camera.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/camera.rpi_camera/
"""
import os import os
import subprocess import subprocess
import logging import logging
@ -43,7 +47,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class RaspberryCamera(Camera): class RaspberryCamera(Camera):
"""Raspberry Pi camera.""" """Representation of a Raspberry Pi camera."""
def __init__(self, device_info): def __init__(self, device_info):
"""Initialize Raspberry Pi camera component.""" """Initialize Raspberry Pi camera component."""

View File

@ -7,9 +7,10 @@ https://home-assistant.io/components/garage_door.wink/
import logging import logging
from homeassistant.components.garage_door import GarageDoorDevice from homeassistant.components.garage_door import GarageDoorDevice
from homeassistant.const import CONF_ACCESS_TOKEN, ATTR_BATTERY_LEVEL from homeassistant.components.wink import WinkDevice
from homeassistant.const import CONF_ACCESS_TOKEN
REQUIREMENTS = ['python-wink==0.7.7'] REQUIREMENTS = ['python-wink==0.7.8', 'pubnub==3.7.8']
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
@ -31,38 +32,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
pywink.get_garage_doors()) pywink.get_garage_doors())
class WinkGarageDoorDevice(GarageDoorDevice): class WinkGarageDoorDevice(WinkDevice, GarageDoorDevice):
"""Representation of a Wink garage door.""" """Representation of a Wink garage door."""
def __init__(self, wink): def __init__(self, wink):
"""Initialize the garage door.""" """Initialize the garage door."""
self.wink = wink WinkDevice.__init__(self, wink)
self._battery = self.wink.battery_level
@property
def unique_id(self):
"""Return the ID of this wink garage door."""
return "{}.{}".format(self.__class__, self.wink.device_id())
@property
def name(self):
"""Return the name of the garage door if any."""
return self.wink.name()
def update(self):
"""Update the state of the garage door."""
self.wink.update_state()
@property @property
def is_closed(self): def is_closed(self):
"""Return true if door is closed.""" """Return true if door is closed."""
return self.wink.state() == 0 return self.wink.state() == 0
@property
def available(self):
"""True if connection == True."""
return self.wink.available
def close_door(self): def close_door(self):
"""Close the door.""" """Close the door."""
self.wink.set_state(0) self.wink.set_state(0)
@ -70,16 +51,3 @@ class WinkGarageDoorDevice(GarageDoorDevice):
def open_door(self): def open_door(self):
"""Open the door.""" """Open the door."""
self.wink.set_state(1) self.wink.set_state(1)
@property
def device_state_attributes(self):
"""Return the state attributes."""
if self._battery:
return {
ATTR_BATTERY_LEVEL: self._battery_level,
}
@property
def _battery_level(self):
"""Return the battery level."""
return self.wink.battery_level * 100

View File

@ -1,17 +1,8 @@
""" """
Support for Homematic Devices. Support for Homematic devices.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/homematic/ https://home-assistant.io/components/homematic/
Configuration:
homematic:
local_ip: "<IP of device running Home Assistant>"
local_port: <Port for connection with Home Assistant>
remote_ip: "<IP of Homegear / CCU>"
remote_port: <Port of Homegear / CCU XML-RPC Server>
autodetect: "<True/False>" (optional, experimental, detect all devices)
""" """
import time import time
import logging import logging
@ -21,11 +12,10 @@ from homeassistant.helpers import discovery
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
DOMAIN = 'homematic' DOMAIN = 'homematic'
REQUIREMENTS = ['pyhomematic==0.1.6'] REQUIREMENTS = ['pyhomematic==0.1.8']
HOMEMATIC = None HOMEMATIC = None
HOMEMATIC_LINK_DELAY = 0.5 HOMEMATIC_LINK_DELAY = 0.5
HOMEMATIC_DEVICES = {}
DISCOVER_SWITCHES = "homematic.switch" DISCOVER_SWITCHES = "homematic.switch"
DISCOVER_LIGHTS = "homematic.light" DISCOVER_LIGHTS = "homematic.light"
@ -35,18 +25,22 @@ DISCOVER_ROLLERSHUTTER = "homematic.rollershutter"
DISCOVER_THERMOSTATS = "homematic.thermostat" DISCOVER_THERMOSTATS = "homematic.thermostat"
ATTR_DISCOVER_DEVICES = "devices" ATTR_DISCOVER_DEVICES = "devices"
ATTR_DISCOVER_CONFIG = "config" ATTR_PARAM = "param"
ATTR_CHANNEL = "channel"
ATTR_NAME = "name"
ATTR_ADDRESS = "address"
EVENT_KEYPRESS = "homematic.keypress"
HM_DEVICE_TYPES = { HM_DEVICE_TYPES = {
DISCOVER_SWITCHES: ["Switch", "SwitchPowermeter"], DISCOVER_SWITCHES: ["Switch", "SwitchPowermeter"],
DISCOVER_LIGHTS: ["Dimmer"], DISCOVER_LIGHTS: ["Dimmer"],
DISCOVER_SENSORS: ["SwitchPowermeter", "Motion", "MotionV2", DISCOVER_SENSORS: ["SwitchPowermeter", "Motion", "MotionV2",
"RemoteMotion", "ThermostatWall", "AreaThermostat", "RemoteMotion", "ThermostatWall", "AreaThermostat",
"RotaryHandleSensor"], "RotaryHandleSensor", "WaterSensor"],
DISCOVER_THERMOSTATS: ["Thermostat", "ThermostatWall", "MAXThermostat"], DISCOVER_THERMOSTATS: ["Thermostat", "ThermostatWall", "MAXThermostat"],
DISCOVER_BINARY_SENSORS: ["Remote", "ShutterContact", "Smoke", "SmokeV2", DISCOVER_BINARY_SENSORS: ["ShutterContact", "Smoke", "SmokeV2",
"Motion", "MotionV2", "RemoteMotion", "Motion", "MotionV2", "RemoteMotion"],
"GongSensor"],
DISCOVER_ROLLERSHUTTER: ["Blind"] DISCOVER_ROLLERSHUTTER: ["Blind"]
} }
@ -66,6 +60,13 @@ HM_ATTRIBUTE_SUPPORT = {
"VOLTAGE": ["Voltage", {}] "VOLTAGE": ["Voltage", {}]
} }
HM_PRESS_EVENTS = [
"PRESS_SHORT",
"PRESS_LONG",
"PRESS_CONT",
"PRESS_LONG_RELEASE"
]
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -81,6 +82,8 @@ def setup(hass, config):
remote_ip = config[DOMAIN].get("remote_ip", None) remote_ip = config[DOMAIN].get("remote_ip", None)
remote_port = config[DOMAIN].get("remote_port", 2001) remote_port = config[DOMAIN].get("remote_port", 2001)
resolvenames = config[DOMAIN].get("resolvenames", False) resolvenames = config[DOMAIN].get("resolvenames", False)
username = config[DOMAIN].get("username", "Admin")
password = config[DOMAIN].get("password", "")
HOMEMATIC_LINK_DELAY = config[DOMAIN].get("delay", 0.5) HOMEMATIC_LINK_DELAY = config[DOMAIN].get("delay", 0.5)
if remote_ip is None or local_ip is None: if remote_ip is None or local_ip is None:
@ -89,12 +92,15 @@ def setup(hass, config):
# Create server thread # Create server thread
bound_system_callback = partial(system_callback_handler, hass, config) bound_system_callback = partial(system_callback_handler, hass, config)
# pylint: disable=unexpected-keyword-arg
HOMEMATIC = HMConnection(local=local_ip, HOMEMATIC = HMConnection(local=local_ip,
localport=local_port, localport=local_port,
remote=remote_ip, remote=remote_ip,
remoteport=remote_port, remoteport=remote_port,
systemcallback=bound_system_callback, systemcallback=bound_system_callback,
resolvenames=resolvenames, resolvenames=resolvenames,
rpcusername=username,
rpcpassword=password,
interface_id="homeassistant") interface_id="homeassistant")
# Start server thread, connect to peer, initialize to receive events # Start server thread, connect to peer, initialize to receive events
@ -119,22 +125,23 @@ def system_callback_handler(hass, config, src, *args):
for dev in dev_descriptions: for dev in dev_descriptions:
key_dict[dev['ADDRESS'].split(':')[0]] = True key_dict[dev['ADDRESS'].split(':')[0]] = True
# Connect devices already created in HA to pyhomematic and # Register EVENTS
# add remaining devices to list # Search all device with a EVENTNODE that include data
devices_not_created = [] bound_event_callback = partial(_hm_event_handler, hass)
for dev in key_dict: for dev in key_dict:
if dev in HOMEMATIC_DEVICES: if dev not in HOMEMATIC.devices:
for hm_element in HOMEMATIC_DEVICES[dev]: continue
hm_element.link_homematic()
else: hmdevice = HOMEMATIC.devices.get(dev)
devices_not_created.append(dev) # have events?
if len(hmdevice.EVENTNODE) > 0:
_LOGGER.debug("Register Events from %s", dev)
hmdevice.setEventCallback(callback=bound_event_callback,
bequeath=True)
# If configuration allows autodetection of devices, # If configuration allows autodetection of devices,
# all devices not configured are added. # all devices not configured are added.
autodetect = config[DOMAIN].get("autodetect", False) if key_dict:
_LOGGER.debug("Autodetect is %s / unknown device: %s", str(autodetect),
str(devices_not_created))
if autodetect and devices_not_created:
for component_name, discovery_type in ( for component_name, discovery_type in (
('switch', DISCOVER_SWITCHES), ('switch', DISCOVER_SWITCHES),
('light', DISCOVER_LIGHTS), ('light', DISCOVER_LIGHTS),
@ -143,8 +150,7 @@ def system_callback_handler(hass, config, src, *args):
('sensor', DISCOVER_SENSORS), ('sensor', DISCOVER_SENSORS),
('thermostat', DISCOVER_THERMOSTATS)): ('thermostat', DISCOVER_THERMOSTATS)):
# Get all devices of a specific type # Get all devices of a specific type
found_devices = _get_devices(discovery_type, found_devices = _get_devices(discovery_type, key_dict)
devices_not_created)
# When devices of this type are found # When devices of this type are found
# they are setup in HA and an event is fired # they are setup in HA and an event is fired
@ -156,32 +162,25 @@ def system_callback_handler(hass, config, src, *args):
def _get_devices(device_type, keys): def _get_devices(device_type, keys):
"""Get devices.""" """Get the Homematic devices."""
from homeassistant.components.binary_sensor.homematic import \
SUPPORT_HM_EVENT_AS_BINMOD
# run # run
device_arr = [] device_arr = []
if not keys:
keys = HOMEMATIC.devices
for key in keys: for key in keys:
device = HOMEMATIC.devices[key] device = HOMEMATIC.devices[key]
if device.__class__.__name__ not in HM_DEVICE_TYPES[device_type]: class_name = device.__class__.__name__
continue
metadata = {} metadata = {}
# is class supported by discovery type
if class_name not in HM_DEVICE_TYPES[device_type]:
continue
# Load metadata if needed to generate a param list # Load metadata if needed to generate a param list
if device_type == DISCOVER_SENSORS: if device_type == DISCOVER_SENSORS:
metadata.update(device.SENSORNODE) metadata.update(device.SENSORNODE)
elif device_type == DISCOVER_BINARY_SENSORS: elif device_type == DISCOVER_BINARY_SENSORS:
metadata.update(device.BINARYNODE) metadata.update(device.BINARYNODE)
# Also add supported events as binary type params = _create_params_list(device, metadata, device_type)
for event, channel in device.EVENTNODE.items():
if event in SUPPORT_HM_EVENT_AS_BINMOD:
metadata.update({event: channel})
params = _create_params_list(device, metadata)
if params: if params:
# Generate options for 1...n elements with 1...n params # Generate options for 1...n elements with 1...n params
for channel in range(1, device.ELEMENT + 1): for channel in range(1, device.ELEMENT + 1):
@ -194,9 +193,9 @@ def _get_devices(device_type, keys):
device_dict = dict(platform="homematic", device_dict = dict(platform="homematic",
address=key, address=key,
name=name, name=name,
button=channel) channel=channel)
if param is not None: if param is not None:
device_dict["param"] = param device_dict[ATTR_PARAM] = param
# Add new device # Add new device
device_arr.append(device_dict) device_arr.append(device_dict)
@ -209,15 +208,22 @@ def _get_devices(device_type, keys):
return device_arr return device_arr
def _create_params_list(hmdevice, metadata): def _create_params_list(hmdevice, metadata, device_type):
"""Create a list from HMDevice with all possible parameters in config.""" """Create a list from HMDevice with all possible parameters in config."""
params = {} params = {}
merge = False
# use merge?
if device_type == DISCOVER_SENSORS:
merge = True
elif device_type == DISCOVER_BINARY_SENSORS:
merge = True
# Search in sensor and binary metadata per elements # Search in sensor and binary metadata per elements
for channel in range(1, hmdevice.ELEMENT + 1): for channel in range(1, hmdevice.ELEMENT + 1):
param_chan = [] param_chan = []
try: for node, meta_chan in metadata.items():
for node, meta_chan in metadata.items(): try:
# Is this attribute ignored? # Is this attribute ignored?
if node in HM_IGNORE_DISCOVERY_NODE: if node in HM_IGNORE_DISCOVERY_NODE:
continue continue
@ -227,15 +233,17 @@ def _create_params_list(hmdevice, metadata):
elif channel == 1: elif channel == 1:
# First channel can have other data channel # First channel can have other data channel
param_chan.append(node) param_chan.append(node)
# pylint: disable=broad-except except (TypeError, ValueError):
except Exception as err: _LOGGER.error("Exception generating %s (%s)",
_LOGGER.error("Exception generating %s (%s): %s", hmdevice.ADDRESS, str(metadata))
hmdevice.ADDRESS, str(metadata), str(err))
# Default parameter # default parameter is merge is off
if not param_chan: if len(param_chan) == 0 and not merge:
param_chan.append(None) param_chan.append(None)
# Add to channel # Add to channel
params.update({channel: param_chan}) if len(param_chan) > 0:
params.update({channel: param_chan})
_LOGGER.debug("Create param list for %s with: %s", hmdevice.ADDRESS, _LOGGER.debug("Create param list for %s with: %s", hmdevice.ADDRESS,
str(params)) str(params))
@ -264,55 +272,55 @@ def _create_ha_name(name, channel, param):
def setup_hmdevice_discovery_helper(hmdevicetype, discovery_info, def setup_hmdevice_discovery_helper(hmdevicetype, discovery_info,
add_callback_devices): add_callback_devices):
"""Helper to setup Homematic devices with discovery info.""" """Helper to setup Homematic devices with discovery info."""
for config in discovery_info["devices"]: for config in discovery_info[ATTR_DISCOVER_DEVICES]:
ret = setup_hmdevice_entity_helper(hmdevicetype, config, _LOGGER.debug("Add device %s from config: %s",
add_callback_devices) str(hmdevicetype), str(config))
if not ret:
_LOGGER.error("Setup discovery error with config %s", str(config)) # create object and add to HA
new_device = hmdevicetype(config)
add_callback_devices([new_device])
# link to HM
new_device.link_homematic()
return True return True
def setup_hmdevice_entity_helper(hmdevicetype, config, add_callback_devices): def _hm_event_handler(hass, device, caller, attribute, value):
"""Helper to setup Homematic devices.""" """Handle all pyhomematic device events."""
if HOMEMATIC is None: channel = device.split(":")[1]
_LOGGER.error('Error setting up HMDevice: Server not configured.') address = device.split(":")[0]
return False hmdevice = HOMEMATIC.devices.get(address)
address = config.get('address', None) # is not a event?
if address is None: if attribute not in hmdevice.EVENTNODE:
_LOGGER.error("Error setting up device '%s': " + return
"'address' missing in configuration.", address)
return False
_LOGGER.debug("Add device %s from config: %s", _LOGGER.debug("Event %s for %s channel %s", attribute,
str(hmdevicetype), str(config)) hmdevice.NAME, channel)
# Create a new HA homematic object
new_device = hmdevicetype(config)
if address not in HOMEMATIC_DEVICES:
HOMEMATIC_DEVICES[address] = []
HOMEMATIC_DEVICES[address].append(new_device)
# Add to HA # a keypress event
add_callback_devices([new_device]) if attribute in HM_PRESS_EVENTS:
hass.bus.fire(EVENT_KEYPRESS, {
ATTR_NAME: hmdevice.NAME,
ATTR_PARAM: attribute,
ATTR_CHANNEL: channel
})
return
# HM is connected _LOGGER.warning("Event is unknown and not forwarded to HA")
if address in HOMEMATIC.devices:
return new_device.link_homematic()
return True
class HMDevice(Entity): class HMDevice(Entity):
"""Homematic device base object.""" """The Homematic device base object."""
# pylint: disable=too-many-instance-attributes # pylint: disable=too-many-instance-attributes
def __init__(self, config): def __init__(self, config):
"""Initialize generic HM device.""" """Initialize a generic Homematic device."""
self._name = config.get("name", None) self._name = config.get(ATTR_NAME, None)
self._address = config.get("address", None) self._address = config.get(ATTR_ADDRESS, None)
self._channel = config.get("button", 1) self._channel = config.get(ATTR_CHANNEL, 1)
self._state = config.get("param", None) self._state = config.get(ATTR_PARAM, None)
self._hidden = config.get("hidden", False)
self._data = {} self._data = {}
self._hmdevice = None self._hmdevice = None
self._connected = False self._connected = False
@ -330,7 +338,7 @@ class HMDevice(Entity):
@property @property
def should_poll(self): def should_poll(self):
"""Return False. Homematic states are pushed by the XML RPC Server.""" """Return false. Homematic states are pushed by the XML RPC Server."""
return False return False
@property @property
@ -340,24 +348,23 @@ class HMDevice(Entity):
@property @property
def assumed_state(self): def assumed_state(self):
"""Return True if unable to access real state of the device.""" """Return true if unable to access real state of the device."""
return not self._available return not self._available
@property @property
def available(self): def available(self):
"""Return True if device is available.""" """Return true if device is available."""
return self._available return self._available
@property
def hidden(self):
"""Return True if the entity should be hidden from UIs."""
return self._hidden
@property @property
def device_state_attributes(self): def device_state_attributes(self):
"""Return device specific state attributes.""" """Return device specific state attributes."""
attr = {} attr = {}
# no data available to create
if not self.available:
return attr
# Generate an attributes list # Generate an attributes list
for node, data in HM_ATTRIBUTE_SUPPORT.items(): for node, data in HM_ATTRIBUTE_SUPPORT.items():
# Is an attributes and exists for this object # Is an attributes and exists for this object
@ -365,10 +372,13 @@ class HMDevice(Entity):
value = data[1].get(self._data[node], self._data[node]) value = data[1].get(self._data[node], self._data[node])
attr[data[0]] = value attr[data[0]] = value
# static attributes
attr["ID"] = self._hmdevice.ADDRESS
return attr return attr
def link_homematic(self): def link_homematic(self):
"""Connect to homematic.""" """Connect to Homematic."""
# device is already linked # device is already linked
if self._connected: if self._connected:
return True return True
@ -379,7 +389,7 @@ class HMDevice(Entity):
self._hmdevice = HOMEMATIC.devices[self._address] self._hmdevice = HOMEMATIC.devices[self._address]
self._connected = True self._connected = True
# Check if HM class is okay for HA class # Check if Homematic class is okay for HA class
_LOGGER.info("Start linking %s to %s", self._address, self._name) _LOGGER.info("Start linking %s to %s", self._address, self._name)
if self._check_hm_to_ha_object(): if self._check_hm_to_ha_object():
try: try:
@ -402,7 +412,7 @@ class HMDevice(Entity):
_LOGGER.error("Exception while linking %s: %s", _LOGGER.error("Exception while linking %s: %s",
self._address, str(err)) self._address, str(err))
else: else:
_LOGGER.critical("Delink %s object from HM!", self._name) _LOGGER.critical("Delink %s object from HM", self._name)
self._connected = False self._connected = False
# Update HA # Update HA
@ -429,18 +439,12 @@ class HMDevice(Entity):
self._available = bool(value) self._available = bool(value)
have_change = True have_change = True
# If it has changed, update HA # If it has changed data point, update HA
if have_change: if have_change:
_LOGGER.debug("%s update_ha_state after '%s'", self._name, _LOGGER.debug("%s update_ha_state after '%s'", self._name,
attribute) attribute)
self.update_ha_state() self.update_ha_state()
# Reset events
if attribute in self._hmdevice.EVENTNODE:
_LOGGER.debug("%s reset event", self._name)
self._data[attribute] = False
self.update_ha_state()
def _subscribe_homematic_events(self): def _subscribe_homematic_events(self):
"""Subscribe all required events to handle job.""" """Subscribe all required events to handle job."""
channels_to_sub = {} channels_to_sub = {}
@ -488,24 +492,21 @@ class HMDevice(Entity):
if node in self._data: if node in self._data:
self._data[node] = funct(name=node, channel=self._channel) self._data[node] = funct(name=node, channel=self._channel)
# Set events to False
for node in self._hmdevice.EVENTNODE:
if node in self._data:
self._data[node] = False
return True return True
def _hm_set_state(self, value): def _hm_set_state(self, value):
"""Set data to main datapoint."""
if self._state in self._data: if self._state in self._data:
self._data[self._state] = value self._data[self._state] = value
def _hm_get_state(self): def _hm_get_state(self):
"""Get data from main datapoint."""
if self._state in self._data: if self._state in self._data:
return self._data[self._state] return self._data[self._state]
return None return None
def _check_hm_to_ha_object(self): def _check_hm_to_ha_object(self):
"""Check if it is possible to use the HM Object as this HA type. """Check if it is possible to use the Homematic object as this HA type.
NEEDS overwrite by inherit! NEEDS overwrite by inherit!
""" """
@ -521,7 +522,7 @@ class HMDevice(Entity):
return True return True
def _init_data_struct(self): def _init_data_struct(self):
"""Generate a data dict (self._data) from hm metadata. """Generate a data dict (self._data) from the Homematic metadata.
NEEDS overwrite by inherit! NEEDS overwrite by inherit!
""" """

View File

@ -13,19 +13,19 @@ import re
import ssl import ssl
import voluptuous as vol import voluptuous as vol
import homeassistant.core as ha
import homeassistant.remote as rem import homeassistant.remote as rem
from homeassistant import util from homeassistant import util
from homeassistant.const import ( from homeassistant.const import (
SERVER_PORT, HTTP_HEADER_HA_AUTH, HTTP_HEADER_CACHE_CONTROL, SERVER_PORT, HTTP_HEADER_HA_AUTH, HTTP_HEADER_CACHE_CONTROL,
HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN,
HTTP_HEADER_ACCESS_CONTROL_ALLOW_HEADERS, ALLOWED_CORS_HEADERS) HTTP_HEADER_ACCESS_CONTROL_ALLOW_HEADERS, ALLOWED_CORS_HEADERS,
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START)
from homeassistant.helpers.entity import split_entity_id from homeassistant.helpers.entity import split_entity_id
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
DOMAIN = "http" DOMAIN = "http"
REQUIREMENTS = ("eventlet==0.19.0", "static3==0.7.0", "Werkzeug==0.11.5") REQUIREMENTS = ("cherrypy==6.0.2", "static3==0.7.0", "Werkzeug==0.11.10")
CONF_API_PASSWORD = "api_password" CONF_API_PASSWORD = "api_password"
CONF_SERVER_HOST = "server_host" CONF_SERVER_HOST = "server_host"
@ -40,7 +40,8 @@ DATA_API_PASSWORD = 'api_password'
# TLS configuation follows the best-practice guidelines # TLS configuation follows the best-practice guidelines
# specified here: https://wiki.mozilla.org/Security/Server_Side_TLS # specified here: https://wiki.mozilla.org/Security/Server_Side_TLS
# Intermediate guidelines are followed. # Intermediate guidelines are followed.
SSL_VERSION = ssl.PROTOCOL_TLSv1 SSL_VERSION = ssl.PROTOCOL_SSLv23
SSL_OPTS = ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 | ssl.OP_NO_COMPRESSION
CIPHERS = "ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:" \ CIPHERS = "ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:" \
"ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:" \ "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:" \
"ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:" \ "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:" \
@ -117,11 +118,17 @@ def setup(hass, config):
cors_origins=cors_origins cors_origins=cors_origins
) )
hass.bus.listen_once( def start_wsgi_server(event):
ha.EVENT_HOMEASSISTANT_START, """Start the WSGI server."""
lambda event: server.start()
threading.Thread(target=server.start, daemon=True,
name='WSGI-server').start()) hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_wsgi_server)
def stop_wsgi_server(event):
"""Stop the WSGI server."""
server.stop()
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_wsgi_server)
hass.wsgi = server hass.wsgi = server
hass.config.api = rem.API(server_host if server_host != '0.0.0.0' hass.config.api = rem.API(server_host if server_host != '0.0.0.0'
@ -240,6 +247,7 @@ class HomeAssistantWSGI(object):
self.server_port = server_port self.server_port = server_port
self.cors_origins = cors_origins self.cors_origins = cors_origins
self.event_forwarder = None self.event_forwarder = None
self.server = None
def register_view(self, view): def register_view(self, view):
"""Register a view with the WSGI server. """Register a view with the WSGI server.
@ -307,15 +315,34 @@ class HomeAssistantWSGI(object):
def start(self): def start(self):
"""Start the wsgi server.""" """Start the wsgi server."""
from eventlet import wsgi from cherrypy import wsgiserver
import eventlet from cherrypy.wsgiserver.ssl_builtin import BuiltinSSLAdapter
# pylint: disable=too-few-public-methods,super-init-not-called
class ContextSSLAdapter(BuiltinSSLAdapter):
"""SSL Adapter that takes in an SSL context."""
def __init__(self, context):
self.context = context
# pylint: disable=no-member
self.server = wsgiserver.CherryPyWSGIServer(
(self.server_host, self.server_port), self,
server_name='Home Assistant')
sock = eventlet.listen((self.server_host, self.server_port))
if self.ssl_certificate: if self.ssl_certificate:
sock = eventlet.wrap_ssl(sock, certfile=self.ssl_certificate, context = ssl.SSLContext(SSL_VERSION)
keyfile=self.ssl_key, server_side=True, context.options |= SSL_OPTS
ssl_version=SSL_VERSION, ciphers=CIPHERS) context.set_ciphers(CIPHERS)
wsgi.server(sock, self, log=_LOGGER) context.load_cert_chain(self.ssl_certificate, self.ssl_key)
self.server.ssl_adapter = ContextSSLAdapter(context)
threading.Thread(target=self.server.start, daemon=True,
name='WSGI-server').start()
def stop(self):
"""Stop the wsgi server."""
self.server.stop()
def dispatch_request(self, request): def dispatch_request(self, request):
"""Handle incoming request.""" """Handle incoming request."""
@ -362,6 +389,10 @@ class HomeAssistantWSGI(object):
"""Handle a request for base app + extra apps.""" """Handle a request for base app + extra apps."""
from werkzeug.wsgi import DispatcherMiddleware from werkzeug.wsgi import DispatcherMiddleware
if not self.hass.is_running:
from werkzeug.exceptions import BadRequest
return BadRequest()(environ, start_response)
app = DispatcherMiddleware(self.base_app, self.extra_apps) app = DispatcherMiddleware(self.base_app, self.extra_apps)
# Strip out any cachebusting MD5 fingerprints # Strip out any cachebusting MD5 fingerprints
fingerprinted = _FINGERPRINT.match(environ.get('PATH_INFO', '')) fingerprinted = _FINGERPRINT.match(environ.get('PATH_INFO', ''))

View File

@ -23,7 +23,7 @@ DEFAULT_DATABASE = 'home_assistant'
DEFAULT_SSL = False DEFAULT_SSL = False
DEFAULT_VERIFY_SSL = False DEFAULT_VERIFY_SSL = False
REQUIREMENTS = ['influxdb==2.12.0'] REQUIREMENTS = ['influxdb==3.0.0']
CONF_HOST = 'host' CONF_HOST = 'host'
CONF_PORT = 'port' CONF_PORT = 'port'

View File

@ -4,7 +4,6 @@ Support for EnOcean light sources.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.enocean/ https://home-assistant.io/components/light.enocean/
""" """
import logging import logging
import math import math
@ -86,7 +85,7 @@ class EnOceanLight(enocean.EnOceanDevice, Light):
self._on_state = False self._on_state = False
def value_changed(self, val): def value_changed(self, val):
"""Update the internal state of this device in HA.""" """Update the internal state of this device."""
self._brightness = math.floor(val / 100.0 * 256.0) self._brightness = math.floor(val / 100.0 * 256.0)
self._on_state = bool(val != 0) self._on_state = bool(val != 0)
self.update_ha_state() self.update_ha_state()

View File

@ -1,21 +1,9 @@
""" """
The homematic light platform. Support for Homematic lighs.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.homematic/ https://home-assistant.io/components/light.homematic/
Important: For this platform to work the homematic component has to be
properly configured.
Configuration:
light:
- platform: homematic
addresss: <Homematic addresss for device> # e.g. "JEQ0XXXXXXX"
name: <User defined name> (optional)
button: n (integer of channel to map, device-dependent)
""" """
import logging import logging
from homeassistant.components.light import (ATTR_BRIGHTNESS, Light) from homeassistant.components.light import (ATTR_BRIGHTNESS, Light)
from homeassistant.const import STATE_UNKNOWN from homeassistant.const import STATE_UNKNOWN
@ -23,24 +11,21 @@ import homeassistant.components.homematic as homematic
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
# List of component names (string) your component depends upon.
DEPENDENCIES = ['homematic'] DEPENDENCIES = ['homematic']
def setup_platform(hass, config, add_callback_devices, discovery_info=None): def setup_platform(hass, config, add_callback_devices, discovery_info=None):
"""Setup the platform.""" """Setup the Homematic light platform."""
if discovery_info: if discovery_info is None:
return homematic.setup_hmdevice_discovery_helper(HMLight, return
discovery_info,
add_callback_devices) return homematic.setup_hmdevice_discovery_helper(HMLight,
# Manual discovery_info,
return homematic.setup_hmdevice_entity_helper(HMLight, add_callback_devices)
config,
add_callback_devices)
class HMLight(homematic.HMDevice, Light): class HMLight(homematic.HMDevice, Light):
"""Represents a Homematic Light in Home Assistant.""" """Representation of a Homematic light."""
@property @property
def brightness(self): def brightness(self):
@ -55,7 +40,7 @@ class HMLight(homematic.HMDevice, Light):
@property @property
def is_on(self): def is_on(self):
"""Return True if light is on.""" """Return true if light is on."""
try: try:
return self._hm_get_state() > 0 return self._hm_get_state() > 0
except TypeError: except TypeError:
@ -78,24 +63,24 @@ class HMLight(homematic.HMDevice, Light):
self._hmdevice.off(self._channel) self._hmdevice.off(self._channel)
def _check_hm_to_ha_object(self): def _check_hm_to_ha_object(self):
"""Check if possible to use the HM Object as this HA type.""" """Check if possible to use the Homematic object as this HA type."""
from pyhomematic.devicetypes.actors import Dimmer, Switch from pyhomematic.devicetypes.actors import Dimmer, Switch
# Check compatibility from HMDevice # Check compatibility from HMDevice
if not super()._check_hm_to_ha_object(): if not super()._check_hm_to_ha_object():
return False return False
# Check if the homematic device is correct for this HA device # Check if the Homematic device is correct for this HA device
if isinstance(self._hmdevice, Switch): if isinstance(self._hmdevice, Switch):
return True return True
if isinstance(self._hmdevice, Dimmer): if isinstance(self._hmdevice, Dimmer):
return True return True
_LOGGER.critical("This %s can't be use as light!", self._name) _LOGGER.critical("This %s can't be use as light", self._name)
return False return False
def _init_data_struct(self): def _init_data_struct(self):
"""Generate a data dict (self._data) from hm metadata.""" """Generate a data dict (self._data) from the Homematic metadata."""
from pyhomematic.devicetypes.actors import Dimmer, Switch from pyhomematic.devicetypes.actors import Dimmer, Switch
super()._init_data_struct() super()._init_data_struct()

View File

@ -1,19 +1,9 @@
""" """
Support for Osram Lightify. Support for Osram Lightify.
Uses: https://github.com/aneumeier/python-lightify for the Osram light For more details about this platform, please refer to the documentation at
interface. https://home-assistant.io/components/light.osramlightify/
In order to use the platform just add the following to the configuration.yaml:
light:
platform: osramlightify
host: <hostname_or_ip>
Todo:
Add support for Non RGBW lights.
""" """
import logging import logging
import socket import socket
from datetime import timedelta from datetime import timedelta
@ -40,7 +30,7 @@ MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100)
def setup_platform(hass, config, add_devices_callback, discovery_info=None): def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Find and return lights.""" """Setup Osram Lightify lights."""
import lightify import lightify
host = config.get(CONF_HOST) host = config.get(CONF_HOST)
if host: if host:
@ -85,7 +75,7 @@ def setup_bridge(bridge, add_devices_callback):
class OsramLightifyLight(Light): class OsramLightifyLight(Light):
"""Defines an Osram Lightify Light.""" """Representation of an Osram Lightify Light."""
def __init__(self, light_id, light, update_lights): def __init__(self, light_id, light, update_lights):
"""Initialize the light.""" """Initialize the light."""

View File

@ -8,12 +8,13 @@ import logging
from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, \ from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, \
Light, ATTR_RGB_COLOR Light, ATTR_RGB_COLOR
from homeassistant.components.wink import WinkDevice
from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.util import color as color_util from homeassistant.util import color as color_util
from homeassistant.util.color import \ from homeassistant.util.color import \
color_temperature_mired_to_kelvin as mired_to_kelvin color_temperature_mired_to_kelvin as mired_to_kelvin
REQUIREMENTS = ['python-wink==0.7.7'] REQUIREMENTS = ['python-wink==0.7.8', 'pubnub==3.7.8']
def setup_platform(hass, config, add_devices_callback, discovery_info=None): def setup_platform(hass, config, add_devices_callback, discovery_info=None):
@ -35,26 +36,12 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
WinkLight(light) for light in pywink.get_bulbs()) WinkLight(light) for light in pywink.get_bulbs())
class WinkLight(Light): class WinkLight(WinkDevice, Light):
"""Representation of a Wink light.""" """Representation of a Wink light."""
def __init__(self, wink): def __init__(self, wink):
""" """Initialize the Wink device."""
Initialize the light. WinkDevice.__init__(self, wink)
:type wink: pywink.devices.standard.bulb.WinkBulb
"""
self.wink = wink
@property
def unique_id(self):
"""Return the ID of this Wink light."""
return "{}.{}".format(self.__class__, self.wink.device_id())
@property
def name(self):
"""Return the name of the light if any."""
return self.wink.name()
@property @property
def is_on(self): def is_on(self):
@ -66,11 +53,6 @@ class WinkLight(Light):
"""Return the brightness of the light.""" """Return the brightness of the light."""
return int(self.wink.brightness() * 255) return int(self.wink.brightness() * 255)
@property
def available(self):
"""True if connection == True."""
return self.wink.available
@property @property
def xy_color(self): def xy_color(self):
"""Current bulb color in CIE 1931 (XY) color space.""" """Current bulb color in CIE 1931 (XY) color space."""
@ -112,7 +94,3 @@ class WinkLight(Light):
def turn_off(self): def turn_off(self):
"""Turn the switch off.""" """Turn the switch off."""
self.wink.set_state(False) self.wink.set_state(False)
def update(self):
"""Update state of the light."""
self.wink.update_state(require_desired_state_fulfilled=True)

View File

@ -4,12 +4,31 @@ Support for Z-Wave lights.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/light.zwave/ https://home-assistant.io/components/light.zwave/
""" """
import logging
# Because we do not compile openzwave on CI # Because we do not compile openzwave on CI
# pylint: disable=import-error # pylint: disable=import-error
from threading import Timer from threading import Timer
from homeassistant.components.light import ATTR_BRIGHTNESS, DOMAIN, Light from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, \
ATTR_RGB_COLOR, DOMAIN, Light
from homeassistant.components import zwave from homeassistant.components import zwave
from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.util.color import HASS_COLOR_MAX, HASS_COLOR_MIN, \
color_temperature_mired_to_kelvin, color_temperature_to_rgb
_LOGGER = logging.getLogger(__name__)
COLOR_CHANNEL_WARM_WHITE = 0x01
COLOR_CHANNEL_COLD_WHITE = 0x02
COLOR_CHANNEL_RED = 0x04
COLOR_CHANNEL_GREEN = 0x08
COLOR_CHANNEL_BLUE = 0x10
# Generate midpoint color temperatures for bulbs that have limited
# support for white light colors
TEMP_MID_HASS = (HASS_COLOR_MAX - HASS_COLOR_MIN) / 2 + HASS_COLOR_MIN
TEMP_WARM_HASS = (HASS_COLOR_MAX - HASS_COLOR_MIN) / 3 * 2 + HASS_COLOR_MIN
TEMP_COLD_HASS = (HASS_COLOR_MAX - HASS_COLOR_MIN) / 3 + HASS_COLOR_MIN
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
@ -28,7 +47,17 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
return return
value.set_change_verified(False) value.set_change_verified(False)
add_devices([ZwaveDimmer(value)])
if node.has_command_class(zwave.COMMAND_CLASS_COLOR):
try:
add_devices([ZwaveColorLight(value)])
except ValueError as exception:
_LOGGER.warning(
"Error initializing as color bulb: %s "
"Initializing as standard dimmer.", exception)
add_devices([ZwaveDimmer(value)])
else:
add_devices([ZwaveDimmer(value)])
def brightness_state(value): def brightness_state(value):
@ -49,8 +78,9 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
from pydispatch import dispatcher from pydispatch import dispatcher
zwave.ZWaveDeviceEntity.__init__(self, value, DOMAIN) zwave.ZWaveDeviceEntity.__init__(self, value, DOMAIN)
self._brightness = None
self._brightness, self._state = brightness_state(value) self._state = None
self.update_properties()
# Used for value change event handling # Used for value change event handling
self._refreshing = False self._refreshing = False
@ -59,6 +89,11 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
dispatcher.connect( dispatcher.connect(
self._value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED) self._value_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED)
def update_properties(self):
"""Update internal properties based on zwave values."""
# Brightness
self._brightness, self._state = brightness_state(self._value)
def _value_changed(self, value): def _value_changed(self, value):
"""Called when a value has changed on the network.""" """Called when a value has changed on the network."""
if self._value.value_id != value.value_id: if self._value.value_id != value.value_id:
@ -66,7 +101,7 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
if self._refreshing: if self._refreshing:
self._refreshing = False self._refreshing = False
self._brightness, self._state = brightness_state(value) self.update_properties()
else: else:
def _refresh_value(): def _refresh_value():
"""Used timer callback for delayed value refresh.""" """Used timer callback for delayed value refresh."""
@ -107,3 +142,168 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light):
"""Turn the device off.""" """Turn the device off."""
if self._value.node.set_dimmer(self._value.value_id, 0): if self._value.node.set_dimmer(self._value.value_id, 0):
self._state = STATE_OFF self._state = STATE_OFF
def ct_to_rgb(temp):
"""Convert color temperature (mireds) to RGB."""
colorlist = list(
color_temperature_to_rgb(color_temperature_mired_to_kelvin(temp)))
return [int(val) for val in colorlist]
class ZwaveColorLight(ZwaveDimmer):
"""Representation of a Z-Wave color changing light."""
def __init__(self, value):
"""Initialize the light."""
self._value_color = None
self._value_color_channels = None
self._color_channels = None
self._rgb = None
self._ct = None
# Here we attempt to find a zwave color value with the same instance
# id as the dimmer value. Currently zwave nodes that change colors
# only include one dimmer and one color command, but this will
# hopefully provide some forward compatibility for new devices that
# have multiple color changing elements.
for value_color in value.node.get_rgbbulbs().values():
if value.instance == value_color.instance:
self._value_color = value_color
if self._value_color is None:
raise ValueError("No matching color command found.")
for value_color_channels in value.node.get_values(
class_id=zwave.COMMAND_CLASS_COLOR, genre='System',
type="Int").values():
self._value_color_channels = value_color_channels
if self._value_color_channels is None:
raise ValueError("Color Channels not found.")
super().__init__(value)
def update_properties(self):
"""Update internal properties based on zwave values."""
super().update_properties()
# Color Channels
self._color_channels = self._value_color_channels.data
# Color Data String
data = self._value_color.data
# RGB is always present in the openzwave color data string.
self._rgb = [
int(data[1:3], 16),
int(data[3:5], 16),
int(data[5:7], 16)]
# Parse remaining color channels. Openzwave appends white channels
# that are present.
index = 7
# Warm white
if self._color_channels & COLOR_CHANNEL_WARM_WHITE:
warm_white = int(data[index:index+2], 16)
index += 2
else:
warm_white = 0
# Cold white
if self._color_channels & COLOR_CHANNEL_COLD_WHITE:
cold_white = int(data[index:index+2], 16)
index += 2
else:
cold_white = 0
# Color temperature. With two white channels, only two color
# temperatures are supported for the bulb. The channel values
# indicate brightness for warm/cold color temperature.
if (self._color_channels & COLOR_CHANNEL_WARM_WHITE and
self._color_channels & COLOR_CHANNEL_COLD_WHITE):
if warm_white > 0:
self._ct = TEMP_WARM_HASS
self._rgb = ct_to_rgb(self._ct)
elif cold_white > 0:
self._ct = TEMP_COLD_HASS
self._rgb = ct_to_rgb(self._ct)
else:
# RGB color is being used. Just report midpoint.
self._ct = TEMP_MID_HASS
# If only warm white is reported 0-255 is color temperature.
elif self._color_channels & COLOR_CHANNEL_WARM_WHITE:
self._ct = HASS_COLOR_MIN + (HASS_COLOR_MAX - HASS_COLOR_MIN) * (
warm_white / 255)
self._rgb = ct_to_rgb(self._ct)
# If only cold white is reported 0-255 is negative color temperature.
elif self._color_channels & COLOR_CHANNEL_COLD_WHITE:
self._ct = HASS_COLOR_MIN + (HASS_COLOR_MAX - HASS_COLOR_MIN) * (
(255 - cold_white) / 255)
self._rgb = ct_to_rgb(self._ct)
# If no rgb channels supported, report None.
if not (self._color_channels & COLOR_CHANNEL_RED or
self._color_channels & COLOR_CHANNEL_GREEN or
self._color_channels & COLOR_CHANNEL_BLUE):
self._rgb = None
@property
def rgb_color(self):
"""Return the rgb color."""
return self._rgb
@property
def color_temp(self):
"""Return the color temperature."""
return self._ct
def turn_on(self, **kwargs):
"""Turn the device on."""
rgbw = None
if ATTR_COLOR_TEMP in kwargs:
# With two white channels, only two color temperatures are
# supported for the bulb.
if (self._color_channels & COLOR_CHANNEL_WARM_WHITE and
self._color_channels & COLOR_CHANNEL_COLD_WHITE):
if kwargs[ATTR_COLOR_TEMP] > TEMP_MID_HASS:
self._ct = TEMP_WARM_HASS
rgbw = b'#000000FF00'
else:
self._ct = TEMP_COLD_HASS
rgbw = b'#00000000FF'
# If only warm white is reported 0-255 is color temperature
elif self._color_channels & COLOR_CHANNEL_WARM_WHITE:
rgbw = b'#000000'
temp = (
(kwargs[ATTR_COLOR_TEMP] - HASS_COLOR_MIN) /
(HASS_COLOR_MAX - HASS_COLOR_MIN) * 255)
rgbw += format(int(temp)).encode('utf-8')
# If only cold white is reported 0-255 is negative color temp
elif self._color_channels & COLOR_CHANNEL_COLD_WHITE:
rgbw = b'#000000'
temp = (
255 - (kwargs[ATTR_COLOR_TEMP] - HASS_COLOR_MIN) /
(HASS_COLOR_MAX - HASS_COLOR_MIN) * 255)
rgbw += format(int(temp)).encode('utf-8')
elif ATTR_RGB_COLOR in kwargs:
self._rgb = kwargs[ATTR_RGB_COLOR]
rgbw = b'#'
for colorval in self._rgb:
rgbw += format(colorval, '02x').encode('utf-8')
rgbw += b'0000'
if rgbw is None:
_LOGGER.warning("rgbw string was not generated for turn_on")
else:
self._value_color.node.set_rgbw(self._value_color.value_id, rgbw)
super().turn_on(**kwargs)

View File

@ -0,0 +1,65 @@
"""
Support for Vera locks.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/lock.vera/
"""
import logging
from homeassistant.components.lock import LockDevice
from homeassistant.const import (
ATTR_BATTERY_LEVEL, STATE_LOCKED, STATE_UNLOCKED)
from homeassistant.components.vera import (
VeraDevice, VERA_DEVICES, VERA_CONTROLLER)
DEPENDENCIES = ['vera']
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
"""Find and return Vera locks."""
add_devices_callback(
VeraLock(device, VERA_CONTROLLER) for
device in VERA_DEVICES['lock'])
class VeraLock(VeraDevice, LockDevice):
"""Representation of a Vera lock."""
def __init__(self, vera_device, controller):
"""Initialize the Vera device."""
self._state = None
VeraDevice.__init__(self, vera_device, controller)
@property
def device_state_attributes(self):
"""Return the state attributes of the device."""
attr = {}
if self.vera_device.has_battery:
attr[ATTR_BATTERY_LEVEL] = self.vera_device.battery_level + '%'
attr['Vera Device Id'] = self.vera_device.vera_device_id
return attr
def lock(self, **kwargs):
"""Lock the device."""
self.vera_device.lock()
self._state = STATE_LOCKED
self.update_ha_state()
def unlock(self, **kwargs):
"""Unlock the device."""
self.vera_device.unlock()
self._state = STATE_UNLOCKED
self.update_ha_state()
@property
def is_locked(self):
"""Return true if device is on."""
return self._state == STATE_LOCKED
def update(self):
"""Called by the Vera device callback to update state."""
self._state = (STATE_LOCKED if self.vera_device.is_locked(True)
else STATE_UNLOCKED)

View File

@ -7,9 +7,10 @@ https://home-assistant.io/components/lock.wink/
import logging import logging
from homeassistant.components.lock import LockDevice from homeassistant.components.lock import LockDevice
from homeassistant.const import CONF_ACCESS_TOKEN, ATTR_BATTERY_LEVEL from homeassistant.components.wink import WinkDevice
from homeassistant.const import CONF_ACCESS_TOKEN
REQUIREMENTS = ['python-wink==0.7.7'] REQUIREMENTS = ['python-wink==0.7.8', 'pubnub==3.7.8']
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
@ -30,38 +31,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
add_devices(WinkLockDevice(lock) for lock in pywink.get_locks()) add_devices(WinkLockDevice(lock) for lock in pywink.get_locks())
class WinkLockDevice(LockDevice): class WinkLockDevice(WinkDevice, LockDevice):
"""Representation of a Wink lock.""" """Representation of a Wink lock."""
def __init__(self, wink): def __init__(self, wink):
"""Initialize the lock.""" """Initialize the lock."""
self.wink = wink WinkDevice.__init__(self, wink)
self._battery = self.wink.battery_level
@property
def unique_id(self):
"""Return the id of this wink lock."""
return "{}.{}".format(self.__class__, self.wink.device_id())
@property
def name(self):
"""Return the name of the lock if any."""
return self.wink.name()
def update(self):
"""Update the state of the lock."""
self.wink.update_state()
@property @property
def is_locked(self): def is_locked(self):
"""Return true if device is locked.""" """Return true if device is locked."""
return self.wink.state() return self.wink.state()
@property
def available(self):
"""True if connection == True."""
return self.wink.available
def lock(self, **kwargs): def lock(self, **kwargs):
"""Lock the device.""" """Lock the device."""
self.wink.set_state(True) self.wink.set_state(True)
@ -69,16 +50,3 @@ class WinkLockDevice(LockDevice):
def unlock(self, **kwargs): def unlock(self, **kwargs):
"""Unlock the device.""" """Unlock the device."""
self.wink.set_state(False) self.wink.set_state(False)
@property
def device_state_attributes(self):
"""Return the state attributes."""
if self._battery:
return {
ATTR_BATTERY_LEVEL: self._battery_level,
}
@property
def _battery_level(self):
"""Return the battery level."""
return self.wink.battery_level * 100

View File

@ -1,10 +1,8 @@
""" """
Support for interface with a Sony Bravia TV. Support for interface with a Sony Bravia TV.
By Antonio Parraga Navarro For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.braviatv/
dedicated to Isabel
""" """
import logging import logging
import os import os
@ -38,6 +36,7 @@ SUPPORT_BRAVIA = SUPPORT_PAUSE | SUPPORT_VOLUME_STEP | \
def _get_mac_address(ip_address): def _get_mac_address(ip_address):
"""Get the MAC address of the device."""
from subprocess import Popen, PIPE from subprocess import Popen, PIPE
pid = Popen(["arp", "-n", ip_address], stdout=PIPE) pid = Popen(["arp", "-n", ip_address], stdout=PIPE)
@ -48,7 +47,7 @@ def _get_mac_address(ip_address):
def _config_from_file(filename, config=None): def _config_from_file(filename, config=None):
"""Small configuration file management function.""" """Create the configuration from a file."""
if config: if config:
# We're writing configuration # We're writing configuration
bravia_config = _config_from_file(filename) bravia_config = _config_from_file(filename)
@ -104,7 +103,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
# pylint: disable=too-many-branches # pylint: disable=too-many-branches
def setup_bravia(config, pin, hass, add_devices_callback): def setup_bravia(config, pin, hass, add_devices_callback):
"""Setup a sony bravia based on host parameter.""" """Setup a Sony Bravia TV based on host parameter."""
host = config.get(CONF_HOST) host = config.get(CONF_HOST)
name = config.get(CONF_NAME) name = config.get(CONF_NAME)
if name is None: if name is None:
@ -176,7 +175,7 @@ class BraviaTVDevice(MediaPlayerDevice):
"""Representation of a Sony Bravia TV.""" """Representation of a Sony Bravia TV."""
def __init__(self, host, mac, name, pin): def __init__(self, host, mac, name, pin):
"""Initialize the sony bravia device.""" """Initialize the Sony Bravia device."""
from braviarc import braviarc from braviarc import braviarc
self._pin = pin self._pin = pin

View File

@ -2,9 +2,8 @@
Support for interacting with and controlling the cmus music player. Support for interacting with and controlling the cmus music player.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.mpd/ https://home-assistant.io/components/media_player.cmus/
""" """
import logging import logging
from homeassistant.components.media_player import ( from homeassistant.components.media_player import (
@ -17,7 +16,7 @@ from homeassistant.const import (STATE_OFF, STATE_PAUSED, STATE_PLAYING,
CONF_PORT) CONF_PORT)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['pycmus>=0.1.0'] REQUIREMENTS = ['pycmus==0.1.0']
SUPPORT_CMUS = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_TURN_OFF | \ SUPPORT_CMUS = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_TURN_OFF | \
SUPPORT_TURN_ON | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \ SUPPORT_TURN_ON | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \
@ -25,7 +24,7 @@ SUPPORT_CMUS = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_TURN_OFF | \
def setup_platform(hass, config, add_devices, discover_info=None): def setup_platform(hass, config, add_devices, discover_info=None):
"""Setup the Cmus platform.""" """Setup the CMUS platform."""
from pycmus import exceptions from pycmus import exceptions
host = config.get(CONF_HOST, None) host = config.get(CONF_HOST, None)
@ -44,7 +43,7 @@ def setup_platform(hass, config, add_devices, discover_info=None):
class CmusDevice(MediaPlayerDevice): class CmusDevice(MediaPlayerDevice):
"""Representation of a running cmus.""" """Representation of a running CMUS."""
# pylint: disable=no-member, too-many-public-methods, abstract-method # pylint: disable=no-member, too-many-public-methods, abstract-method
def __init__(self, server, password, port, name): def __init__(self, server, password, port, name):

View File

@ -15,7 +15,7 @@ from homeassistant.const import (
STATE_PLAYING, STATE_PAUSED, STATE_OFF) STATE_PLAYING, STATE_PAUSED, STATE_OFF)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['websocket-client==0.35.0'] REQUIREMENTS = ['websocket-client==0.37.0']
SUPPORT_GPMDP = SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK SUPPORT_GPMDP = SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK

View File

@ -15,7 +15,7 @@ from homeassistant.const import (
STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING) STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['jsonrpc-requests==0.2'] REQUIREMENTS = ['jsonrpc-requests==0.3']
SUPPORT_KODI = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ SUPPORT_KODI = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK | \ SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK | \

View File

@ -4,7 +4,6 @@ Support for the roku media player.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.roku/ https://home-assistant.io/components/media_player.roku/
""" """
import logging import logging
from homeassistant.components.media_player import ( from homeassistant.components.media_player import (
@ -77,7 +76,8 @@ class RokuDevice(MediaPlayerDevice):
self.current_app = self.roku.current_app self.current_app = self.roku.current_app
else: else:
self.current_app = None self.current_app = None
except requests.exceptions.ConnectionError: except (requests.exceptions.ConnectionError,
requests.exceptions.ReadTimeout):
self.current_app = None self.current_app = None
def get_source_list(self): def get_source_list(self):

View File

@ -154,12 +154,20 @@ sonos_group_players:
description: Name(s) of entites that will coordinate the grouping. Platform dependent. description: Name(s) of entites that will coordinate the grouping. Platform dependent.
example: 'media_player.living_room_sonos' example: 'media_player.living_room_sonos'
sonos_unjoin:
description: Unjoin the player from a group.
fields:
entity_id:
description: Name(s) of entites that will be unjoined from their group. Platform dependent.
example: 'media_player.living_room_sonos'
sonos_snapshot: sonos_snapshot:
description: Take a snapshot of the media player. description: Take a snapshot of the media player.
fields: fields:
entity_id: entity_id:
description: Name(s) of entites that will coordinate the grouping. Platform dependent. description: Name(s) of entites that will be snapshot. Platform dependent.
example: 'media_player.living_room_sonos' example: 'media_player.living_room_sonos'
sonos_restore: sonos_restore:
@ -167,5 +175,5 @@ sonos_restore:
fields: fields:
entity_id: entity_id:
description: Name(s) of entites that will coordinate the grouping. Platform dependent. description: Name(s) of entites that will be restored. Platform dependent.
example: 'media_player.living_room_sonos' example: 'media_player.living_room_sonos'

View File

@ -4,7 +4,6 @@ Support for interacting with Snapcast clients.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.snapcast/ https://home-assistant.io/components/media_player.snapcast/
""" """
import logging import logging
import socket import socket

View File

@ -34,11 +34,12 @@ SUPPORT_SONOS = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE |\
SUPPORT_SEEK SUPPORT_SEEK
SERVICE_GROUP_PLAYERS = 'sonos_group_players' SERVICE_GROUP_PLAYERS = 'sonos_group_players'
SERVICE_UNJOIN = 'sonos_unjoin'
SERVICE_SNAPSHOT = 'sonos_snapshot' SERVICE_SNAPSHOT = 'sonos_snapshot'
SERVICE_RESTORE = 'sonos_restore' SERVICE_RESTORE = 'sonos_restore'
# pylint: disable=unused-argument # pylint: disable=unused-argument, too-many-locals
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Sonos platform.""" """Setup the Sonos platform."""
import soco import soco
@ -72,47 +73,35 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
add_devices(devices) add_devices(devices)
_LOGGER.info('Added %s Sonos speakers', len(players)) _LOGGER.info('Added %s Sonos speakers', len(players))
def _apply_service(service, service_func, *service_func_args):
"""Internal func for applying a service."""
entity_id = service.data.get('entity_id')
if entity_id:
_devices = [device for device in devices
if device.entity_id == entity_id]
else:
_devices = devices
for device in _devices:
service_func(device, *service_func_args)
device.update_ha_state(True)
def group_players_service(service): def group_players_service(service):
"""Group media players, use player as coordinator.""" """Group media players, use player as coordinator."""
entity_id = service.data.get('entity_id') _apply_service(service, SonosDevice.group_players)
if entity_id: def unjoin_service(service):
_devices = [device for device in devices """Unjoin the player from a group."""
if device.entity_id == entity_id] _apply_service(service, SonosDevice.unjoin)
else:
_devices = devices
for device in _devices: def snapshot_service(service):
device.group_players()
device.update_ha_state(True)
def snapshot(service):
"""Take a snapshot.""" """Take a snapshot."""
entity_id = service.data.get('entity_id') _apply_service(service, SonosDevice.snapshot)
if entity_id: def restore_service(service):
_devices = [device for device in devices
if device.entity_id == entity_id]
else:
_devices = devices
for device in _devices:
device.snapshot(service)
device.update_ha_state(True)
def restore(service):
"""Restore a snapshot.""" """Restore a snapshot."""
entity_id = service.data.get('entity_id') _apply_service(service, SonosDevice.restore)
if entity_id:
_devices = [device for device in devices
if device.entity_id == entity_id]
else:
_devices = devices
for device in _devices:
device.restore(service)
device.update_ha_state(True)
descriptions = load_yaml_config_file( descriptions = load_yaml_config_file(
path.join(path.dirname(__file__), 'services.yaml')) path.join(path.dirname(__file__), 'services.yaml'))
@ -121,12 +110,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
group_players_service, group_players_service,
descriptions.get(SERVICE_GROUP_PLAYERS)) descriptions.get(SERVICE_GROUP_PLAYERS))
hass.services.register(DOMAIN, SERVICE_UNJOIN,
unjoin_service,
descriptions.get(SERVICE_UNJOIN))
hass.services.register(DOMAIN, SERVICE_SNAPSHOT, hass.services.register(DOMAIN, SERVICE_SNAPSHOT,
snapshot, snapshot_service,
descriptions.get(SERVICE_SNAPSHOT)) descriptions.get(SERVICE_SNAPSHOT))
hass.services.register(DOMAIN, SERVICE_RESTORE, hass.services.register(DOMAIN, SERVICE_RESTORE,
restore, restore_service,
descriptions.get(SERVICE_RESTORE)) descriptions.get(SERVICE_RESTORE))
return True return True
@ -356,12 +349,17 @@ class SonosDevice(MediaPlayerDevice):
self._player.partymode() self._player.partymode()
@only_if_coordinator @only_if_coordinator
def snapshot(self, service): def unjoin(self):
"""Unjoin the player from a group."""
self._player.unjoin()
@only_if_coordinator
def snapshot(self):
"""Snapshot the player.""" """Snapshot the player."""
self.soco_snapshot.snapshot() self.soco_snapshot.snapshot()
@only_if_coordinator @only_if_coordinator
def restore(self, service): def restore(self):
"""Restore snapshot for the player.""" """Restore snapshot for the player."""
self.soco_snapshot.restore(True) self.soco_snapshot.restore(True)

View File

@ -4,7 +4,6 @@ Combination of multiple media players into one for a universal controller.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.universal/ https://home-assistant.io/components/media_player.universal/
""" """
import logging import logging
# pylint: disable=import-error # pylint: disable=import-error
from copy import copy from copy import copy

View File

@ -6,13 +6,6 @@ https://home-assistant.io/components/rollershutter.homematic/
Important: For this platform to work the homematic component has to be Important: For this platform to work the homematic component has to be
properly configured. properly configured.
Configuration:
rollershutter:
- platform: homematic
address: "<Homematic address for device>" # e.g. "JEQ0XXXXXXX"
name: "<User defined name>" (optional)
""" """
import logging import logging
@ -29,14 +22,12 @@ DEPENDENCIES = ['homematic']
def setup_platform(hass, config, add_callback_devices, discovery_info=None): def setup_platform(hass, config, add_callback_devices, discovery_info=None):
"""Setup the platform.""" """Setup the platform."""
if discovery_info: if discovery_info is None:
return homematic.setup_hmdevice_discovery_helper(HMRollershutter, return
discovery_info,
add_callback_devices) return homematic.setup_hmdevice_discovery_helper(HMRollershutter,
# Manual discovery_info,
return homematic.setup_hmdevice_entity_helper(HMRollershutter, add_callback_devices)
config,
add_callback_devices)
class HMRollershutter(homematic.HMDevice, RollershutterDevice): class HMRollershutter(homematic.HMDevice, RollershutterDevice):

View File

@ -7,9 +7,10 @@ https://home-assistant.io/components/rollershutter.wink/
import logging import logging
from homeassistant.components.rollershutter import RollershutterDevice from homeassistant.components.rollershutter import RollershutterDevice
from homeassistant.components.wink import WinkDevice
from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.const import CONF_ACCESS_TOKEN
REQUIREMENTS = ['python-wink==0.7.7'] REQUIREMENTS = ['python-wink==0.7.8', 'pubnub==3.7.8']
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
@ -31,38 +32,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
pywink.get_shades()) pywink.get_shades())
class WinkRollershutterDevice(RollershutterDevice): class WinkRollershutterDevice(WinkDevice, RollershutterDevice):
"""Representation of a Wink rollershutter (shades).""" """Representation of a Wink rollershutter (shades)."""
def __init__(self, wink): def __init__(self, wink):
"""Initialize the rollershutter.""" """Initialize the rollershutter."""
self.wink = wink WinkDevice.__init__(self, wink)
self._battery = None
@property @property
def should_poll(self): def should_poll(self):
"""Wink Shades don't track their position.""" """Wink Shades don't track their position."""
return False return False
@property
def unique_id(self):
"""Return the ID of this wink rollershutter."""
return "{}.{}".format(self.__class__, self.wink.device_id())
@property
def name(self):
"""Return the name of the rollershutter if any."""
return self.wink.name()
def update(self):
"""Update the state of the rollershutter."""
return self.wink.update_state()
@property
def available(self):
"""True if connection == True."""
return self.wink.available
def move_down(self): def move_down(self):
"""Close the shade.""" """Close the shade."""
self.wink.set_state(0) self.wink.set_state(0)

View File

@ -1,38 +1,43 @@
""" """
Support for information about the German trans system. Support for information about the German train system.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.deutsche_bahn/ https://home-assistant.io/components/sensor.deutsche_bahn/
""" """
import logging import logging
from datetime import timedelta, datetime from datetime import timedelta
import voluptuous as vol
from homeassistant.const import (CONF_PLATFORM)
import homeassistant.helpers.config_validation as cv
from homeassistant.util import Throttle from homeassistant.util import Throttle
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
_LOGGER = logging.getLogger(__name__)
REQUIREMENTS = ['schiene==0.17'] REQUIREMENTS = ['schiene==0.17']
CONF_START = 'from'
CONF_DESTINATION = 'to'
ICON = 'mdi:train' ICON = 'mdi:train'
_LOGGER = logging.getLogger(__name__)
PLATFORM_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): 'deutsche_bahn',
vol.Required(CONF_START): cv.string,
vol.Required(CONF_DESTINATION): cv.string,
})
# Return cached results if last scan was less then this time ago. # Return cached results if last scan was less then this time ago.
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=120) MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=120)
def setup_platform(hass, config, add_devices_callback, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Deutsche Bahn Sensor.""" """Setup the Deutsche Bahn Sensor."""
start = config.get('from') start = config.get(CONF_START)
goal = config.get('to') destination = config.get(CONF_DESTINATION)
if start is None: add_devices([DeutscheBahnSensor(start, destination)])
_LOGGER.error('Missing required variable: "from"')
return False
if goal is None:
_LOGGER.error('Missing required variable: "to"')
return False
dev = []
dev.append(DeutscheBahnSensor(start, goal))
add_devices_callback(dev)
# pylint: disable=too-few-public-methods # pylint: disable=too-few-public-methods
@ -63,16 +68,17 @@ class DeutscheBahnSensor(Entity):
@property @property
def state_attributes(self): def state_attributes(self):
"""Return the state attributes.""" """Return the state attributes."""
return self.data.connections[0] connections = self.data.connections[0]
connections['next'] = self.data.connections[1]['departure']
connections['next_on'] = self.data.connections[2]['departure']
return connections
def update(self): def update(self):
"""Get the latest delay from bahn.de and updates the state.""" """Get the latest delay from bahn.de and updates the state."""
self.data.update() self.data.update()
self._state = self.data.connections[0].get('departure', 'Unknown') self._state = self.data.connections[0].get('departure', 'Unknown')
if self.data.connections[0]['delay'] != 0: if self.data.connections[0]['delay'] != 0:
self._state += " + {}".format( self._state += " + {}".format(self.data.connections[0]['delay'])
self.data.connections[0]['delay']
)
# pylint: disable=too-few-public-methods # pylint: disable=too-few-public-methods
@ -90,18 +96,15 @@ class SchieneData(object):
@Throttle(MIN_TIME_BETWEEN_UPDATES) @Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self): def update(self):
"""Update the connection data.""" """Update the connection data."""
self.connections = self.schiene.connections(self.start, self.connections = self.schiene.connections(self.start, self.goal)
self.goal,
datetime.now())
for con in self.connections: for con in self.connections:
# Details info is not useful. # Detail info is not useful. Having a more consistent interface
# Having a more consistent interface simplifies # simplifies usage of template sensors.
# usage of Template sensors later on
if 'details' in con: if 'details' in con:
con.pop('details') con.pop('details')
delay = con.get('delay', delay = con.get('delay', {'delay_departure': 0,
{'delay_departure': 0, 'delay_arrival': 0})
'delay_arrival': 0}) # IMHO only delay_departure is useful
# IMHO only delay_departure is usefull
con['delay'] = delay['delay_departure'] con['delay'] = delay['delay_departure']
con['ontime'] = con.get('ontime', False) con['ontime'] = con.get('ontime', False)

View File

@ -6,14 +6,6 @@ https://home-assistant.io/components/sensor.homematic/
Important: For this platform to work the homematic component has to be Important: For this platform to work the homematic component has to be
properly configured. properly configured.
Configuration:
sensor:
- platform: homematic
address: <Homematic address for device> # e.g. "JEQ0XXXXXXX"
name: <User defined name> (optional)
param: <Name of datapoint to us as sensor> (optional)
""" """
import logging import logging
@ -41,14 +33,12 @@ HM_UNIT_HA_CAST = {
def setup_platform(hass, config, add_callback_devices, discovery_info=None): def setup_platform(hass, config, add_callback_devices, discovery_info=None):
"""Setup the platform.""" """Setup the platform."""
if discovery_info: if discovery_info is None:
return homematic.setup_hmdevice_discovery_helper(HMSensor, return
discovery_info,
add_callback_devices) return homematic.setup_hmdevice_discovery_helper(HMSensor,
# Manual discovery_info,
return homematic.setup_hmdevice_entity_helper(HMSensor, add_callback_devices)
config,
add_callback_devices)
class HMSensor(homematic.HMDevice): class HMSensor(homematic.HMDevice):

View File

@ -12,21 +12,24 @@ from glob import glob
from homeassistant.const import STATE_UNKNOWN, TEMP_CELSIUS from homeassistant.const import STATE_UNKNOWN, TEMP_CELSIUS
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
BASE_DIR = '/sys/bus/w1/devices/'
DEVICE_FOLDERS = glob(os.path.join(BASE_DIR, '28*'))
SENSOR_IDS = []
DEVICE_FILES = []
for device_folder in DEVICE_FOLDERS:
SENSOR_IDS.append(os.path.split(device_folder)[1])
DEVICE_FILES.append(os.path.join(device_folder, 'w1_slave'))
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
# pylint: disable=unused-argument # pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the one wire Sensors.""" """Setup the one wire Sensors."""
if DEVICE_FILES == []: base_dir = config.get('mount_dir', '/sys/bus/w1/devices/')
device_folders = glob(os.path.join(base_dir, '[10,22,28,3B,42]*'))
sensor_ids = []
device_files = []
for device_folder in device_folders:
sensor_ids.append(os.path.split(device_folder)[1])
if base_dir.startswith('/sys/bus/w1/devices'):
device_files.append(os.path.join(device_folder, 'w1_slave'))
else:
device_files.append(os.path.join(device_folder, 'temperature'))
if device_files == []:
_LOGGER.error('No onewire sensor found.') _LOGGER.error('No onewire sensor found.')
_LOGGER.error('Check if dtoverlay=w1-gpio,gpiopin=4.') _LOGGER.error('Check if dtoverlay=w1-gpio,gpiopin=4.')
_LOGGER.error('is in your /boot/config.txt and') _LOGGER.error('is in your /boot/config.txt and')
@ -34,7 +37,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
return return
devs = [] devs = []
names = SENSOR_IDS names = sensor_ids
for key in config.keys(): for key in config.keys():
if key == "names": if key == "names":
@ -47,9 +50,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
# map names to ids. # map names to ids.
elif isinstance(config['names'], dict): elif isinstance(config['names'], dict):
names = [] names = []
for sensor_id in SENSOR_IDS: for sensor_id in sensor_ids:
names.append(config['names'].get(sensor_id, sensor_id)) names.append(config['names'].get(sensor_id, sensor_id))
for device_file, name in zip(DEVICE_FILES, names): for device_file, name in zip(device_files, names):
devs.append(OneWire(name, device_file)) devs.append(OneWire(name, device_file))
add_devices(devs) add_devices(devs)
@ -88,14 +91,27 @@ class OneWire(Entity):
def update(self): def update(self):
"""Get the latest data from the device.""" """Get the latest data from the device."""
lines = self._read_temp_raw() temp = -99
while lines[0].strip()[-3:] != 'YES': if self._device_file.startswith('/sys/bus/w1/devices'):
time.sleep(0.2)
lines = self._read_temp_raw() lines = self._read_temp_raw()
equals_pos = lines[1].find('t=') while lines[0].strip()[-3:] != 'YES':
if equals_pos != -1: time.sleep(0.2)
temp_string = lines[1][equals_pos+2:] lines = self._read_temp_raw()
temp = round(float(temp_string) / 1000.0, 1) equals_pos = lines[1].find('t=')
if temp < -55 or temp > 125: if equals_pos != -1:
return temp_string = lines[1][equals_pos+2:]
self._state = temp temp = round(float(temp_string) / 1000.0, 1)
else:
ds_device_file = open(self._device_file, 'r')
temp_read = ds_device_file.readlines()
ds_device_file.close()
if len(temp_read) == 1:
try:
temp = round(float(temp_read[0]), 1)
except ValueError:
_LOGGER.warning('Invalid temperature value read from ' +
self._device_file)
if temp < -55 or temp > 125:
return
self._state = temp

View File

@ -1,4 +1,9 @@
"""Support for openexchangerates.org exchange rates service.""" """
Support for openexchangerates.org exchange rates service.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.openexchangerates/
"""
from datetime import timedelta from datetime import timedelta
import logging import logging
import requests import requests
@ -41,7 +46,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
class OpenexchangeratesSensor(Entity): class OpenexchangeratesSensor(Entity):
"""Implementing the Openexchangerates sensor.""" """Representation of an Openexchangerates sensor."""
def __init__(self, rest, name, quote): def __init__(self, rest, name, quote):
"""Initialize the sensor.""" """Initialize the sensor."""
@ -87,7 +92,7 @@ class OpenexchangeratesData(object):
@Throttle(MIN_TIME_BETWEEN_UPDATES) @Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self): def update(self):
"""Get the latest data from openexchangerates.""" """Get the latest data from openexchangerates.org."""
try: try:
result = requests.get(self._resource, params={'base': self._base, result = requests.get(self._resource, params={'base': self._base,
'app_id': 'app_id':

View File

@ -1,4 +1,9 @@
"""Support for ThinkingCleaner.""" """
Support for ThinkingCleaner.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.thinkingcleaner/
"""
import logging import logging
from datetime import timedelta from datetime import timedelta

View File

@ -7,11 +7,12 @@ at https://home-assistant.io/components/sensor.wink/
import logging import logging
from homeassistant.const import (CONF_ACCESS_TOKEN, STATE_CLOSED, from homeassistant.const import (CONF_ACCESS_TOKEN, STATE_CLOSED,
STATE_OPEN, TEMP_CELSIUS, STATE_OPEN, TEMP_CELSIUS)
ATTR_BATTERY_LEVEL)
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.components.wink import WinkDevice
from homeassistant.loader import get_component
REQUIREMENTS = ['python-wink==0.7.7'] REQUIREMENTS = ['python-wink==0.7.8', 'pubnub==3.7.8']
SENSOR_TYPES = ['temperature', 'humidity'] SENSOR_TYPES = ['temperature', 'humidity']
@ -38,14 +39,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
add_devices(WinkEggMinder(eggtray) for eggtray in pywink.get_eggtrays()) add_devices(WinkEggMinder(eggtray) for eggtray in pywink.get_eggtrays())
class WinkSensorDevice(Entity): class WinkSensorDevice(WinkDevice, Entity):
"""Representation of a Wink sensor.""" """Representation of a Wink sensor."""
def __init__(self, wink): def __init__(self, wink):
"""Initialize the sensor.""" """Initialize the Wink device."""
self.wink = wink super().__init__(wink)
wink = get_component('wink')
self.capability = self.wink.capability() self.capability = self.wink.capability()
self._battery = self.wink.battery_level
if self.wink.UNIT == "°": if self.wink.UNIT == "°":
self._unit_of_measurement = TEMP_CELSIUS self._unit_of_measurement = TEMP_CELSIUS
else: else:
@ -55,9 +56,9 @@ class WinkSensorDevice(Entity):
def state(self): def state(self):
"""Return the state.""" """Return the state."""
if self.capability == "humidity": if self.capability == "humidity":
return self.wink.humidity_percentage() return round(self.wink.humidity_percentage())
elif self.capability == "temperature": elif self.capability == "temperature":
return self.wink.temperature_float() return round(self.wink.temperature_float(), 1)
else: else:
return STATE_OPEN if self.is_open else STATE_CLOSED return STATE_OPEN if self.is_open else STATE_CLOSED
@ -66,80 +67,20 @@ class WinkSensorDevice(Entity):
"""Return the unit of measurement of this entity, if any.""" """Return the unit of measurement of this entity, if any."""
return self._unit_of_measurement return self._unit_of_measurement
@property
def unique_id(self):
"""Return the ID of this wink sensor."""
return "{}.{}".format(self.__class__, self.wink.device_id())
@property
def name(self):
"""Return the name of the sensor if any."""
return self.wink.name()
@property
def available(self):
"""True if connection == True."""
return self.wink.available
def update(self):
"""Update state of the sensor."""
self.wink.update_state()
@property @property
def is_open(self): def is_open(self):
"""Return true if door is open.""" """Return true if door is open."""
return self.wink.state() return self.wink.state()
@property
def device_state_attributes(self):
"""Return the state attributes."""
if self._battery:
return {
ATTR_BATTERY_LEVEL: self._battery_level,
}
@property class WinkEggMinder(WinkDevice, Entity):
def _battery_level(self):
"""Return the battery level."""
return self.wink.battery_level * 100
class WinkEggMinder(Entity):
"""Representation of a Wink Egg Minder.""" """Representation of a Wink Egg Minder."""
def __init__(self, wink): def __init__(self, wink):
"""Initialize the sensor.""" """Initialize the sensor."""
self.wink = wink WinkDevice.__init__(self, wink)
self._battery = self.wink.battery_level
@property @property
def state(self): def state(self):
"""Return the state.""" """Return the state."""
return self.wink.state() return self.wink.state()
@property
def unique_id(self):
"""Return the id of this wink Egg Minder."""
return "{}.{}".format(self.__class__, self.wink.device_id())
@property
def name(self):
"""Return the name of the Egg Minder if any."""
return self.wink.name()
def update(self):
"""Update state of the Egg Minder."""
self.wink.update_state()
@property
def device_state_attributes(self):
"""Return the state attributes."""
if self._battery:
return {
ATTR_BATTERY_LEVEL: self._battery_level,
}
@property
def _battery_level(self):
"""Return the battery level."""
return self.wink.battery_level * 100

View File

@ -15,7 +15,6 @@ from homeassistant.const import (
) )
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from homeassistant.util import location
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -54,16 +53,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Yr.no sensor.""" """Setup the Yr.no sensor."""
latitude = config.get(CONF_LATITUDE, hass.config.latitude) latitude = config.get(CONF_LATITUDE, hass.config.latitude)
longitude = config.get(CONF_LONGITUDE, hass.config.longitude) longitude = config.get(CONF_LONGITUDE, hass.config.longitude)
elevation = config.get(CONF_ELEVATION) elevation = config.get(CONF_ELEVATION, hass.config.elevation or 0)
if None in (latitude, longitude): if None in (latitude, longitude):
_LOGGER.error("Latitude or longitude not set in Home Assistant config") _LOGGER.error("Latitude or longitude not set in Home Assistant config")
return False return False
if elevation is None:
elevation = location.elevation(latitude,
longitude)
coordinates = dict(lat=latitude, coordinates = dict(lat=latitude,
lon=longitude, lon=longitude,
msl=elevation) msl=elevation)

View File

@ -12,7 +12,6 @@ from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import ( from homeassistant.helpers.event import (
track_point_in_utc_time, track_utc_time_change) track_point_in_utc_time, track_utc_time_change)
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from homeassistant.util import location as location_util
from homeassistant.const import CONF_ELEVATION from homeassistant.const import CONF_ELEVATION
REQUIREMENTS = ['astral==1.2'] REQUIREMENTS = ['astral==1.2']
@ -108,7 +107,7 @@ def setup(hass, config):
elevation = platform_config.get(CONF_ELEVATION) elevation = platform_config.get(CONF_ELEVATION)
if elevation is None: if elevation is None:
elevation = location_util.elevation(latitude, longitude) elevation = hass.config.elevation or 0
from astral import Location from astral import Location

View File

@ -1,21 +1,9 @@
""" """
The homematic switch platform. Support for Homematic switches.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/switch.homematic/ https://home-assistant.io/components/switch.homematic/
Important: For this platform to work the homematic component has to be
properly configured.
Configuration:
switch:
- platform: homematic
address: <Homematic address for device> # e.g. "JEQ0XXXXXXX"
name: <User defined name> (optional)
button: n (integer of channel to map, device-dependent) (optional)
""" """
import logging import logging
from homeassistant.components.switch import SwitchDevice from homeassistant.components.switch import SwitchDevice
from homeassistant.const import STATE_UNKNOWN from homeassistant.const import STATE_UNKNOWN
@ -27,19 +15,17 @@ DEPENDENCIES = ['homematic']
def setup_platform(hass, config, add_callback_devices, discovery_info=None): def setup_platform(hass, config, add_callback_devices, discovery_info=None):
"""Setup the platform.""" """Setup the Homematic switch platform."""
if discovery_info: if discovery_info is None:
return homematic.setup_hmdevice_discovery_helper(HMSwitch, return
discovery_info,
add_callback_devices) return homematic.setup_hmdevice_discovery_helper(HMSwitch,
# Manual discovery_info,
return homematic.setup_hmdevice_entity_helper(HMSwitch, add_callback_devices)
config,
add_callback_devices)
class HMSwitch(homematic.HMDevice, SwitchDevice): class HMSwitch(homematic.HMDevice, SwitchDevice):
"""Represents a Homematic Switch in Home Assistant.""" """Representation of a Homematic switch."""
@property @property
def is_on(self): def is_on(self):
@ -71,24 +57,24 @@ class HMSwitch(homematic.HMDevice, SwitchDevice):
self._hmdevice.off(self._channel) self._hmdevice.off(self._channel)
def _check_hm_to_ha_object(self): def _check_hm_to_ha_object(self):
"""Check if possible to use the HM Object as this HA type.""" """Check if possible to use the Homematic object as this HA type."""
from pyhomematic.devicetypes.actors import Dimmer, Switch from pyhomematic.devicetypes.actors import Dimmer, Switch
# Check compatibility from HMDevice # Check compatibility from HMDevice
if not super()._check_hm_to_ha_object(): if not super()._check_hm_to_ha_object():
return False return False
# Check if the homematic device is correct for this HA device # Check if the Homematic device is correct for this HA device
if isinstance(self._hmdevice, Switch): if isinstance(self._hmdevice, Switch):
return True return True
if isinstance(self._hmdevice, Dimmer): if isinstance(self._hmdevice, Dimmer):
return True return True
_LOGGER.critical("This %s can't be use as switch!", self._name) _LOGGER.critical("This %s can't be use as switch", self._name)
return False return False
def _init_data_struct(self): def _init_data_struct(self):
"""Generate a data dict (self._data) from hm metadata.""" """Generate a data dict (self._data) from the Homematic metadata."""
from pyhomematic.devicetypes.actors import Dimmer,\ from pyhomematic.devicetypes.actors import Dimmer,\
Switch, SwitchPowermeter Switch, SwitchPowermeter

View File

@ -1,4 +1,9 @@
"""Support for ThinkingCleaner.""" """
Support for ThinkingCleaner.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/switch.thinkingcleaner/
"""
import time import time
import logging import logging
from datetime import timedelta from datetime import timedelta

View File

@ -52,7 +52,7 @@ class WOLSwitch(SwitchDevice):
@property @property
def is_on(self): def is_on(self):
"""True if switch is on.""" """Return true if switch is on."""
return self._state return self._state
@property @property

View File

@ -6,10 +6,11 @@ https://home-assistant.io/components/switch.wink/
""" """
import logging import logging
from homeassistant.components.wink import WinkToggleDevice from homeassistant.components.wink import WinkDevice
from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.helpers.entity import ToggleEntity
REQUIREMENTS = ['python-wink==0.7.7'] REQUIREMENTS = ['python-wink==0.7.8', 'pubnub==3.7.8']
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
@ -31,3 +32,24 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
add_devices(WinkToggleDevice(switch) for switch in add_devices(WinkToggleDevice(switch) for switch in
pywink.get_powerstrip_outlets()) pywink.get_powerstrip_outlets())
add_devices(WinkToggleDevice(switch) for switch in pywink.get_sirens()) add_devices(WinkToggleDevice(switch) for switch in pywink.get_sirens())
class WinkToggleDevice(WinkDevice, ToggleEntity):
"""Represents a Wink toggle (switch) device."""
def __init__(self, wink):
"""Initialize the Wink device."""
WinkDevice.__init__(self, wink)
@property
def is_on(self):
"""Return true if device is on."""
return self.wink.state()
def turn_on(self, **kwargs):
"""Turn the device on."""
self.wink.set_state(True)
def turn_off(self):
"""Turn the device off."""
self.wink.set_state(False)

View File

@ -1,16 +1,16 @@
""" """
Support for eq3 Bluetooth Smart thermostats. Support for eq3 Bluetooth Smart thermostats.
Uses bluepy_devices library. For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/thermostat.eq3btsmart/
""" """
import logging import logging
from homeassistant.components.thermostat import ThermostatDevice from homeassistant.components.thermostat import ThermostatDevice
from homeassistant.const import TEMP_CELCIUS from homeassistant.const import TEMP_CELCIUS
from homeassistant.helpers.temperature import convert from homeassistant.helpers.temperature import convert
REQUIREMENTS = ['bluepy_devices>=0.2.0'] REQUIREMENTS = ['bluepy_devices==0.2.0']
CONF_MAC = 'mac' CONF_MAC = 'mac'
CONF_DEVICES = 'devices' CONF_DEVICES = 'devices'

View File

@ -1,20 +1,9 @@
""" """
The Homematic thermostat platform. Support for Homematic thermostats.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/thermostat.homematic/ https://home-assistant.io/components/thermostat.homematic/
Important: For this platform to work the homematic component has to be
properly configured.
Configuration:
thermostat:
- platform: homematic
address: "<Homematic address for device>" # e.g. "JEQ0XXXXXXX"
name: "<User defined name>" (optional)
""" """
import logging import logging
import homeassistant.components.homematic as homematic import homeassistant.components.homematic as homematic
from homeassistant.components.thermostat import ThermostatDevice from homeassistant.components.thermostat import ThermostatDevice
@ -27,20 +16,18 @@ _LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_callback_devices, discovery_info=None): def setup_platform(hass, config, add_callback_devices, discovery_info=None):
"""Setup the platform.""" """Setup the Homematic thermostat platform."""
if discovery_info: if discovery_info is None:
return homematic.setup_hmdevice_discovery_helper(HMThermostat, return
discovery_info,
add_callback_devices) return homematic.setup_hmdevice_discovery_helper(HMThermostat,
# Manual discovery_info,
return homematic.setup_hmdevice_entity_helper(HMThermostat, add_callback_devices)
config,
add_callback_devices)
# pylint: disable=abstract-method # pylint: disable=abstract-method
class HMThermostat(homematic.HMDevice, ThermostatDevice): class HMThermostat(homematic.HMDevice, ThermostatDevice):
"""Represents a Homematic Thermostat in Home Assistant.""" """Representation of a Homematic thermostat."""
@property @property
def unit_of_measurement(self): def unit_of_measurement(self):
@ -78,7 +65,7 @@ class HMThermostat(homematic.HMDevice, ThermostatDevice):
return convert(30.5, TEMP_CELSIUS, self.unit_of_measurement) return convert(30.5, TEMP_CELSIUS, self.unit_of_measurement)
def _check_hm_to_ha_object(self): def _check_hm_to_ha_object(self):
"""Check if possible to use the HM Object as this HA type.""" """Check if possible to use the Homematic object as this HA type."""
from pyhomematic.devicetypes.thermostats import HMThermostat\ from pyhomematic.devicetypes.thermostats import HMThermostat\
as pyHMThermostat as pyHMThermostat
@ -86,7 +73,7 @@ class HMThermostat(homematic.HMDevice, ThermostatDevice):
if not super()._check_hm_to_ha_object(): if not super()._check_hm_to_ha_object():
return False return False
# Check if the homematic device correct for this HA device # Check if the Homematic device correct for this HA device
if isinstance(self._hmdevice, pyHMThermostat): if isinstance(self._hmdevice, pyHMThermostat):
return True return True
@ -94,7 +81,7 @@ class HMThermostat(homematic.HMDevice, ThermostatDevice):
return False return False
def _init_data_struct(self): def _init_data_struct(self):
"""Generate a data dict (self._data) from hm metadata.""" """Generate a data dict (self._data) from the Homematic metadata."""
super()._init_data_struct() super()._init_data_struct()
# Add state to data dict # Add state to data dict

View File

@ -13,12 +13,13 @@ from homeassistant.helpers import discovery
from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['pyvera==0.2.10'] REQUIREMENTS = ['pyvera==0.2.12']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DOMAIN = 'vera' DOMAIN = 'vera'
VERA_CONTROLLER = None VERA_CONTROLLER = None
CONF_EXCLUDE = 'exclude' CONF_EXCLUDE = 'exclude'
@ -33,6 +34,7 @@ DEVICE_CATEGORIES = {
'Switch': 'switch', 'Switch': 'switch',
'Armable Sensor': 'switch', 'Armable Sensor': 'switch',
'On/Off Switch': 'switch', 'On/Off Switch': 'switch',
'Doorlock': 'lock',
# 'Window Covering': NOT SUPPORTED YET # 'Window Covering': NOT SUPPORTED YET
} }
@ -91,7 +93,7 @@ def setup(hass, base_config):
dev_type = 'light' dev_type = 'light'
VERA_DEVICES[dev_type].append(device) VERA_DEVICES[dev_type].append(device)
for component in 'binary_sensor', 'sensor', 'light', 'switch': for component in 'binary_sensor', 'sensor', 'light', 'switch', 'lock':
discovery.load_platform(hass, component, DOMAIN, {}, base_config) discovery.load_platform(hass, component, DOMAIN, {}, base_config)
return True return True

View File

@ -5,13 +5,17 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/components/wink/ https://home-assistant.io/components/wink/
""" """
import logging import logging
import json
from homeassistant.const import CONF_ACCESS_TOKEN, ATTR_BATTERY_LEVEL
from homeassistant.helpers import validate_config, discovery from homeassistant.helpers import validate_config, discovery
from homeassistant.helpers.entity import ToggleEntity from homeassistant.const import CONF_ACCESS_TOKEN, ATTR_BATTERY_LEVEL
from homeassistant.helpers.entity import Entity
DOMAIN = "wink" DOMAIN = "wink"
REQUIREMENTS = ['python-wink==0.7.7'] REQUIREMENTS = ['python-wink==0.7.8', 'pubnub==3.7.8']
SUBSCRIPTION_HANDLER = None
CHANNELS = []
def setup(hass, config): def setup(hass, config):
@ -22,7 +26,11 @@ def setup(hass, config):
return False return False
import pywink import pywink
from pubnub import Pubnub
pywink.set_bearer_token(config[DOMAIN][CONF_ACCESS_TOKEN]) pywink.set_bearer_token(config[DOMAIN][CONF_ACCESS_TOKEN])
global SUBSCRIPTION_HANDLER
SUBSCRIPTION_HANDLER = Pubnub("N/A", pywink.get_subscription_key())
SUBSCRIPTION_HANDLER.set_heartbeat(120)
# Load components for the devices in the Wink that we support # Load components for the devices in the Wink that we support
for component_name, func_exists in ( for component_name, func_exists in (
@ -41,13 +49,33 @@ def setup(hass, config):
return True return True
class WinkToggleDevice(ToggleEntity): class WinkDevice(Entity):
"""Represents a Wink toggle (switch) device.""" """Represents a base Wink device."""
def __init__(self, wink): def __init__(self, wink):
"""Initialize the Wink device.""" """Initialize the Wink device."""
from pubnub import Pubnub
self.wink = wink self.wink = wink
self._battery = self.wink.battery_level self._battery = self.wink.battery_level
if self.wink.pubnub_channel in CHANNELS:
pubnub = Pubnub("N/A", self.wink.pubnub_key)
pubnub.set_heartbeat(120)
pubnub.subscribe(self.wink.pubnub_channel,
self._pubnub_update,
error=self._pubnub_error)
else:
CHANNELS.append(self.wink.pubnub_channel)
SUBSCRIPTION_HANDLER.subscribe(self.wink.pubnub_channel,
self._pubnub_update,
error=self._pubnub_error)
def _pubnub_update(self, message, channel):
self.wink.pubnub_update(json.loads(message))
self.update_ha_state()
def _pubnub_error(self, message):
logging.getLogger(__name__).error(
"Error on pubnub update for " + self.wink.name())
@property @property
def unique_id(self): def unique_id(self):
@ -59,28 +87,20 @@ class WinkToggleDevice(ToggleEntity):
"""Return the name of the device.""" """Return the name of the device."""
return self.wink.name() return self.wink.name()
@property
def is_on(self):
"""Return true if device is on."""
return self.wink.state()
@property @property
def available(self): def available(self):
"""True if connection == True.""" """True if connection == True."""
return self.wink.available return self.wink.available
def turn_on(self, **kwargs):
"""Turn the device on."""
self.wink.set_state(True)
def turn_off(self):
"""Turn the device off."""
self.wink.set_state(False)
def update(self): def update(self):
"""Update state of the device.""" """Update state of the device."""
self.wink.update_state() self.wink.update_state()
@property
def should_poll(self):
"""Only poll if we are not subscribed to pubnub."""
return self.wink.pubnub_channel is None
@property @property
def device_state_attributes(self): def device_state_attributes(self):
"""Return the state attributes.""" """Return the state attributes."""

View File

@ -41,6 +41,7 @@ EVENT_SCENE_ACTIVATED = "zwave.scene_activated"
COMMAND_CLASS_WHATEVER = None COMMAND_CLASS_WHATEVER = None
COMMAND_CLASS_SENSOR_MULTILEVEL = 49 COMMAND_CLASS_SENSOR_MULTILEVEL = 49
COMMAND_CLASS_COLOR = 51
COMMAND_CLASS_METER = 50 COMMAND_CLASS_METER = 50
COMMAND_CLASS_ALARM = 113 COMMAND_CLASS_ALARM = 113
COMMAND_CLASS_SWITCH_BINARY = 37 COMMAND_CLASS_SWITCH_BINARY = 37

View File

@ -1,31 +1,35 @@
"""Module to help with parsing and generating configuration files.""" """Module to help with parsing and generating configuration files."""
import logging import logging
import os import os
import shutil
from types import MappingProxyType from types import MappingProxyType
import voluptuous as vol import voluptuous as vol
import homeassistant.util.location as loc_util
from homeassistant.const import ( from homeassistant.const import (
CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, CONF_TEMPERATURE_UNIT, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, CONF_TEMPERATURE_UNIT,
CONF_TIME_ZONE, CONF_CUSTOMIZE) CONF_TIME_ZONE, CONF_CUSTOMIZE, CONF_ELEVATION, TEMP_FAHRENHEIT,
TEMP_CELSIUS, __version__)
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.util.yaml import load_yaml from homeassistant.util.yaml import load_yaml
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import valid_entity_id from homeassistant.helpers.entity import valid_entity_id, set_customize
from homeassistant.util import dt as date_util, location as loc_util
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
YAML_CONFIG_FILE = 'configuration.yaml' YAML_CONFIG_FILE = 'configuration.yaml'
VERSION_FILE = '.HA_VERSION'
CONFIG_DIR_NAME = '.homeassistant' CONFIG_DIR_NAME = '.homeassistant'
DEFAULT_CONFIG = ( DEFAULT_CONFIG = (
# Tuples (attribute, default, auto detect property, description) # Tuples (attribute, default, auto detect property, description)
(CONF_NAME, 'Home', None, 'Name of the location where Home Assistant is ' (CONF_NAME, 'Home', None, 'Name of the location where Home Assistant is '
'running'), 'running'),
(CONF_LATITUDE, None, 'latitude', 'Location required to calculate the time' (CONF_LATITUDE, 0, 'latitude', 'Location required to calculate the time'
' the sun rises and sets'), ' the sun rises and sets'),
(CONF_LONGITUDE, None, 'longitude', None), (CONF_LONGITUDE, 0, 'longitude', None),
(CONF_ELEVATION, 0, None, 'Impacts weather/sunrise data'),
(CONF_TEMPERATURE_UNIT, 'C', None, 'C for Celsius, F for Fahrenheit'), (CONF_TEMPERATURE_UNIT, 'C', None, 'C for Celsius, F for Fahrenheit'),
(CONF_TIME_ZONE, 'UTC', 'time_zone', 'Pick yours from here: http://en.wiki' (CONF_TIME_ZONE, 'UTC', 'time_zone', 'Pick yours from here: http://en.wiki'
'pedia.org/wiki/List_of_tz_database_time_zones'), 'pedia.org/wiki/List_of_tz_database_time_zones'),
@ -39,7 +43,7 @@ DEFAULT_COMPONENTS = {
'history:': 'Enables support for tracking state changes over time.', 'history:': 'Enables support for tracking state changes over time.',
'logbook:': 'View all events in a logbook', 'logbook:': 'View all events in a logbook',
'sun:': 'Track the sun', 'sun:': 'Track the sun',
'sensor:\n platform: yr': 'Prediction of weather', 'sensor:\n platform: yr': 'Weather Prediction',
} }
@ -61,6 +65,7 @@ CORE_CONFIG_SCHEMA = vol.Schema({
CONF_NAME: vol.Coerce(str), CONF_NAME: vol.Coerce(str),
CONF_LATITUDE: cv.latitude, CONF_LATITUDE: cv.latitude,
CONF_LONGITUDE: cv.longitude, CONF_LONGITUDE: cv.longitude,
CONF_ELEVATION: vol.Coerce(float),
CONF_TEMPERATURE_UNIT: cv.temperature_unit, CONF_TEMPERATURE_UNIT: cv.temperature_unit,
CONF_TIME_ZONE: cv.time_zone, CONF_TIME_ZONE: cv.time_zone,
vol.Required(CONF_CUSTOMIZE, vol.Required(CONF_CUSTOMIZE,
@ -97,6 +102,7 @@ def create_default_config(config_dir, detect_location=True):
Return path to new config file if success, None if failed. Return path to new config file if success, None if failed.
""" """
config_path = os.path.join(config_dir, YAML_CONFIG_FILE) config_path = os.path.join(config_dir, YAML_CONFIG_FILE)
version_path = os.path.join(config_dir, VERSION_FILE)
info = {attr: default for attr, default, _, _ in DEFAULT_CONFIG} info = {attr: default for attr, default, _, _ in DEFAULT_CONFIG}
@ -111,6 +117,10 @@ def create_default_config(config_dir, detect_location=True):
continue continue
info[attr] = getattr(location_info, prop) or default info[attr] = getattr(location_info, prop) or default
if location_info.latitude and location_info.longitude:
info[CONF_ELEVATION] = loc_util.elevation(location_info.latitude,
location_info.longitude)
# Writing files with YAML does not create the most human readable results # Writing files with YAML does not create the most human readable results
# So we're hard coding a YAML template. # So we're hard coding a YAML template.
try: try:
@ -130,6 +140,9 @@ def create_default_config(config_dir, detect_location=True):
config_file.write("# {}\n".format(description)) config_file.write("# {}\n".format(description))
config_file.write("{}\n\n".format(component)) config_file.write("{}\n\n".format(component))
with open(version_path, 'wt') as version_file:
version_file.write(__version__)
return config_path return config_path
except IOError: except IOError:
@ -155,3 +168,112 @@ def load_yaml_config_file(config_path):
raise HomeAssistantError(msg) raise HomeAssistantError(msg)
return conf_dict return conf_dict
def process_ha_config_upgrade(hass):
"""Upgrade config if necessary."""
version_path = hass.config.path(VERSION_FILE)
try:
with open(version_path, 'rt') as inp:
conf_version = inp.readline().strip()
except FileNotFoundError:
# Last version to not have this file
conf_version = '0.7.7'
if conf_version == __version__:
return
_LOGGER.info('Upgrading config directory from %s to %s', conf_version,
__version__)
lib_path = hass.config.path('deps')
if os.path.isdir(lib_path):
shutil.rmtree(lib_path)
with open(version_path, 'wt') as outp:
outp.write(__version__)
def process_ha_core_config(hass, config):
"""Process the [homeassistant] section from the config."""
# pylint: disable=too-many-branches
config = CORE_CONFIG_SCHEMA(config)
hac = hass.config
def set_time_zone(time_zone_str):
"""Helper method to set time zone."""
if time_zone_str is None:
return
time_zone = date_util.get_time_zone(time_zone_str)
if time_zone:
hac.time_zone = time_zone
date_util.set_default_time_zone(time_zone)
else:
_LOGGER.error('Received invalid time zone %s', time_zone_str)
for key, attr in ((CONF_LATITUDE, 'latitude'),
(CONF_LONGITUDE, 'longitude'),
(CONF_NAME, 'location_name'),
(CONF_ELEVATION, 'elevation')):
if key in config:
setattr(hac, attr, config[key])
if CONF_TIME_ZONE in config:
set_time_zone(config.get(CONF_TIME_ZONE))
set_customize(config.get(CONF_CUSTOMIZE) or {})
if CONF_TEMPERATURE_UNIT in config:
hac.temperature_unit = config[CONF_TEMPERATURE_UNIT]
# Shortcut if no auto-detection necessary
if None not in (hac.latitude, hac.longitude, hac.temperature_unit,
hac.time_zone, hac.elevation):
return
discovered = []
# If we miss some of the needed values, auto detect them
if None in (hac.latitude, hac.longitude, hac.temperature_unit,
hac.time_zone):
info = loc_util.detect_location_info()
if info is None:
_LOGGER.error('Could not detect location information')
return
if hac.latitude is None and hac.longitude is None:
hac.latitude = info.latitude
hac.longitude = info.longitude
discovered.append(('latitude', hac.latitude))
discovered.append(('longitude', hac.longitude))
if hac.temperature_unit is None:
if info.use_fahrenheit:
hac.temperature_unit = TEMP_FAHRENHEIT
discovered.append(('temperature_unit', 'F'))
else:
hac.temperature_unit = TEMP_CELSIUS
discovered.append(('temperature_unit', 'C'))
if hac.location_name is None:
hac.location_name = info.city
discovered.append(('name', info.city))
if hac.time_zone is None:
set_time_zone(info.time_zone)
discovered.append(('time_zone', info.time_zone))
if hac.elevation is None and hac.latitude is not None and \
hac.longitude is not None:
elevation = loc_util.elevation(hac.latitude, hac.longitude)
hac.elevation = elevation
discovered.append(('elevation', elevation))
if discovered:
_LOGGER.warning(
'Incomplete core config. Auto detected %s',
', '.join('{}: {}'.format(key, val) for key, val in discovered))

View File

@ -49,6 +49,19 @@ MIN_WORKER_THREAD = 2
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
class CoreState(enum.Enum):
"""Represent the current state of Home Assistant."""
not_running = "NOT_RUNNING"
starting = "STARTING"
running = "RUNNING"
stopping = "STOPPING"
def __str__(self):
"""Return the event."""
return self.value
class HomeAssistant(object): class HomeAssistant(object):
"""Root object of the Home Assistant home automation.""" """Root object of the Home Assistant home automation."""
@ -59,14 +72,23 @@ class HomeAssistant(object):
self.services = ServiceRegistry(self.bus, pool) self.services = ServiceRegistry(self.bus, pool)
self.states = StateMachine(self.bus) self.states = StateMachine(self.bus)
self.config = Config() self.config = Config()
self.state = CoreState.not_running
@property
def is_running(self):
"""Return if Home Assistant is running."""
return self.state == CoreState.running
def start(self): def start(self):
"""Start home assistant.""" """Start home assistant."""
_LOGGER.info( _LOGGER.info(
"Starting Home Assistant (%d threads)", self.pool.worker_count) "Starting Home Assistant (%d threads)", self.pool.worker_count)
self.state = CoreState.starting
create_timer(self) create_timer(self)
self.bus.fire(EVENT_HOMEASSISTANT_START) self.bus.fire(EVENT_HOMEASSISTANT_START)
self.pool.block_till_done()
self.state = CoreState.running
def block_till_stopped(self): def block_till_stopped(self):
"""Register service homeassistant/stop and will block until called.""" """Register service homeassistant/stop and will block until called."""
@ -113,8 +135,10 @@ class HomeAssistant(object):
def stop(self): def stop(self):
"""Stop Home Assistant and shuts down all threads.""" """Stop Home Assistant and shuts down all threads."""
_LOGGER.info("Stopping") _LOGGER.info("Stopping")
self.state = CoreState.stopping
self.bus.fire(EVENT_HOMEASSISTANT_STOP) self.bus.fire(EVENT_HOMEASSISTANT_STOP)
self.pool.stop() self.pool.stop()
self.state = CoreState.not_running
class JobPriority(util.OrderedEnum): class JobPriority(util.OrderedEnum):
@ -681,6 +705,7 @@ class Config(object):
"""Initialize a new config object.""" """Initialize a new config object."""
self.latitude = None self.latitude = None
self.longitude = None self.longitude = None
self.elevation = None
self.temperature_unit = None self.temperature_unit = None
self.location_name = None self.location_name = None
self.time_zone = None self.time_zone = None

View File

@ -11,6 +11,7 @@ from datetime import datetime
import enum import enum
import json import json
import logging import logging
import time
import threading import threading
import urllib.parse import urllib.parse
@ -123,6 +124,7 @@ class HomeAssistant(ha.HomeAssistant):
self.services = ha.ServiceRegistry(self.bus, pool) self.services = ha.ServiceRegistry(self.bus, pool)
self.states = StateMachine(self.bus, self.remote_api) self.states = StateMachine(self.bus, self.remote_api)
self.config = ha.Config() self.config = ha.Config()
self.state = ha.CoreState.not_running
self.config.api = local_api self.config.api = local_api
@ -134,17 +136,20 @@ class HomeAssistant(ha.HomeAssistant):
raise HomeAssistantError( raise HomeAssistantError(
'Unable to setup local API to receive events') 'Unable to setup local API to receive events')
self.state = ha.CoreState.starting
ha.create_timer(self) ha.create_timer(self)
self.bus.fire(ha.EVENT_HOMEASSISTANT_START, self.bus.fire(ha.EVENT_HOMEASSISTANT_START,
origin=ha.EventOrigin.remote) origin=ha.EventOrigin.remote)
# Give eventlet time to startup # Ensure local HTTP is started
import eventlet self.pool.block_till_done()
eventlet.sleep(0.1) self.state = ha.CoreState.running
time.sleep(0.05)
# Setup that events from remote_api get forwarded to local_api # Setup that events from remote_api get forwarded to local_api
# Do this after we fire START, otherwise HTTP is not started # Do this after we are running, otherwise HTTP is not started
# or requests are blocked
if not connect_remote_events(self.remote_api, self.config.api): if not connect_remote_events(self.remote_api, self.config.api):
raise HomeAssistantError(( raise HomeAssistantError((
'Could not setup event forwarding from api {} to ' 'Could not setup event forwarding from api {} to '
@ -153,6 +158,7 @@ class HomeAssistant(ha.HomeAssistant):
def stop(self): def stop(self):
"""Stop Home Assistant and shuts down all threads.""" """Stop Home Assistant and shuts down all threads."""
_LOGGER.info("Stopping") _LOGGER.info("Stopping")
self.state = ha.CoreState.stopping
self.bus.fire(ha.EVENT_HOMEASSISTANT_STOP, self.bus.fire(ha.EVENT_HOMEASSISTANT_STOP,
origin=ha.EventOrigin.remote) origin=ha.EventOrigin.remote)
@ -161,6 +167,7 @@ class HomeAssistant(ha.HomeAssistant):
# Disconnect master event forwarding # Disconnect master event forwarding
disconnect_remote_events(self.remote_api, self.config.api) disconnect_remote_events(self.remote_api, self.config.api)
self.state = ha.CoreState.not_running
class EventBus(ha.EventBus): class EventBus(ha.EventBus):

View File

@ -8,7 +8,8 @@ import math
import requests import requests
ELEVATION_URL = 'http://maps.googleapis.com/maps/api/elevation/json' ELEVATION_URL = 'http://maps.googleapis.com/maps/api/elevation/json'
DATA_SOURCE = ['https://freegeoip.io/json/', 'http://ip-api.com/json'] FREEGEO_API = 'https://freegeoip.io/json/'
IP_API = 'http://ip-api.com/json'
# Constants from https://github.com/maurycyp/vincenty # Constants from https://github.com/maurycyp/vincenty
# Earth ellipsoid according to WGS 84 # Earth ellipsoid according to WGS 84
@ -32,30 +33,13 @@ LocationInfo = collections.namedtuple(
def detect_location_info(): def detect_location_info():
"""Detect location information.""" """Detect location information."""
success = None data = _get_freegeoip()
for source in DATA_SOURCE: if data is None:
try: data = _get_ip_api()
raw_info = requests.get(source, timeout=5).json()
success = source
break
except (requests.RequestException, ValueError):
success = False
if success is False: if data is None:
return None return None
else:
data = {key: raw_info.get(key) for key in LocationInfo._fields}
if success is DATA_SOURCE[1]:
data['ip'] = raw_info.get('query')
data['country_code'] = raw_info.get('countryCode')
data['country_name'] = raw_info.get('country')
data['region_code'] = raw_info.get('region')
data['region_name'] = raw_info.get('regionName')
data['zip_code'] = raw_info.get('zip')
data['time_zone'] = raw_info.get('timezone')
data['latitude'] = raw_info.get('lat')
data['longitude'] = raw_info.get('lon')
# From Wikipedia: Fahrenheit is used in the Bahamas, Belize, # From Wikipedia: Fahrenheit is used in the Bahamas, Belize,
# the Cayman Islands, Palau, and the United States and associated # the Cayman Islands, Palau, and the United States and associated
@ -73,11 +57,16 @@ def distance(lat1, lon1, lat2, lon2):
def elevation(latitude, longitude): def elevation(latitude, longitude):
"""Return elevation for given latitude and longitude.""" """Return elevation for given latitude and longitude."""
req = requests.get(ELEVATION_URL, try:
params={'locations': '{},{}'.format(latitude, req = requests.get(
longitude), ELEVATION_URL,
'sensor': 'false'}, params={
timeout=10) 'locations': '{},{}'.format(latitude, longitude),
'sensor': 'false',
},
timeout=10)
except requests.RequestException:
return 0
if req.status_code != 200: if req.status_code != 200:
return 0 return 0
@ -157,3 +146,45 @@ def vincenty(point1, point2, miles=False):
s *= MILES_PER_KILOMETER # kilometers to miles s *= MILES_PER_KILOMETER # kilometers to miles
return round(s, 6) return round(s, 6)
def _get_freegeoip():
"""Query freegeoip.io for location data."""
try:
raw_info = requests.get(FREEGEO_API, timeout=5).json()
except (requests.RequestException, ValueError):
return None
return {
'ip': raw_info.get('ip'),
'country_code': raw_info.get('country_code'),
'country_name': raw_info.get('country_name'),
'region_code': raw_info.get('region_code'),
'region_name': raw_info.get('region_name'),
'city': raw_info.get('city'),
'zip_code': raw_info.get('zip_code'),
'time_zone': raw_info.get('time_zone'),
'latitude': raw_info.get('latitude'),
'longitude': raw_info.get('longitude'),
}
def _get_ip_api():
"""Query ip-api.com for location data."""
try:
raw_info = requests.get(IP_API, timeout=5).json()
except (requests.RequestException, ValueError):
return None
return {
'ip': raw_info.get('query'),
'country_code': raw_info.get('countryCode'),
'country_name': raw_info.get('country'),
'region_code': raw_info.get('region'),
'region_name': raw_info.get('regionName'),
'city': raw_info.get('city'),
'zip_code': raw_info.get('zip'),
'time_zone': raw_info.get('timezone'),
'latitude': raw_info.get('lat'),
'longitude': raw_info.get('lon'),
}

View File

@ -5,7 +5,6 @@ pytz>=2016.4
pip>=7.0.0 pip>=7.0.0
jinja2>=2.8 jinja2>=2.8
voluptuous==0.8.9 voluptuous==0.8.9
eventlet==0.19.0
# homeassistant.components.isy994 # homeassistant.components.isy994
PyISY==1.0.6 PyISY==1.0.6
@ -23,7 +22,7 @@ SoCo==0.11.1
TwitterAPI==2.4.1 TwitterAPI==2.4.1
# homeassistant.components.http # homeassistant.components.http
Werkzeug==0.11.5 Werkzeug==0.11.10
# homeassistant.components.apcupsd # homeassistant.components.apcupsd
apcaccess==0.0.4 apcaccess==0.0.4
@ -41,13 +40,16 @@ blinkstick==1.1.7
blockchain==1.3.3 blockchain==1.3.3
# homeassistant.components.thermostat.eq3btsmart # homeassistant.components.thermostat.eq3btsmart
# bluepy_devices>=0.2.0 # bluepy_devices==0.2.0
# homeassistant.components.notify.aws_lambda # homeassistant.components.notify.aws_lambda
# homeassistant.components.notify.aws_sns # homeassistant.components.notify.aws_sns
# homeassistant.components.notify.aws_sqs # homeassistant.components.notify.aws_sqs
boto3==1.3.1 boto3==1.3.1
# homeassistant.components.http
cherrypy==6.0.2
# homeassistant.components.notify.xmpp # homeassistant.components.notify.xmpp
dnspython3==1.12.0 dnspython3==1.12.0
@ -61,9 +63,6 @@ eliqonline==1.0.12
# homeassistant.components.enocean # homeassistant.components.enocean
enocean==0.31 enocean==0.31
# homeassistant.components.http
eventlet==0.19.0
# homeassistant.components.thermostat.honeywell # homeassistant.components.thermostat.honeywell
evohomeclient==0.2.5 evohomeclient==0.2.5
@ -163,13 +162,13 @@ https://github.com/w1ll1am23/pygooglevoice-sms/archive/7c5ee9969b97a7992fc86a753
https://github.com/wokar/pylgnetcast/archive/v0.2.0.zip#pylgnetcast==0.2.0 https://github.com/wokar/pylgnetcast/archive/v0.2.0.zip#pylgnetcast==0.2.0
# homeassistant.components.influxdb # homeassistant.components.influxdb
influxdb==2.12.0 influxdb==3.0.0
# homeassistant.components.insteon_hub # homeassistant.components.insteon_hub
insteon_hub==0.4.5 insteon_hub==0.4.5
# homeassistant.components.media_player.kodi # homeassistant.components.media_player.kodi
jsonrpc-requests==0.2 jsonrpc-requests==0.3
# homeassistant.components.light.lifx # homeassistant.components.light.lifx
liffylights==0.9.4 liffylights==0.9.4
@ -220,6 +219,16 @@ proliphix==0.1.0
# homeassistant.components.sensor.systemmonitor # homeassistant.components.sensor.systemmonitor
psutil==4.3.0 psutil==4.3.0
# homeassistant.components.wink
# homeassistant.components.binary_sensor.wink
# homeassistant.components.garage_door.wink
# homeassistant.components.light.wink
# homeassistant.components.lock.wink
# homeassistant.components.rollershutter.wink
# homeassistant.components.sensor.wink
# homeassistant.components.switch.wink
pubnub==3.7.8
# homeassistant.components.notify.pushbullet # homeassistant.components.notify.pushbullet
pushbullet.py==0.10.0 pushbullet.py==0.10.0
@ -245,7 +254,7 @@ pyasn1==0.1.9
pychromecast==0.7.2 pychromecast==0.7.2
# homeassistant.components.media_player.cmus # homeassistant.components.media_player.cmus
pycmus>=0.1.0 pycmus==0.1.0
# homeassistant.components.envisalink # homeassistant.components.envisalink
# homeassistant.components.zwave # homeassistant.components.zwave
@ -258,7 +267,7 @@ pyenvisalink==1.0
pyfttt==0.3 pyfttt==0.3
# homeassistant.components.homematic # homeassistant.components.homematic
pyhomematic==0.1.6 pyhomematic==0.1.8
# homeassistant.components.device_tracker.icloud # homeassistant.components.device_tracker.icloud
pyicloud==0.8.3 pyicloud==0.8.3
@ -324,13 +333,13 @@ python-twitch==1.2.0
# homeassistant.components.rollershutter.wink # homeassistant.components.rollershutter.wink
# homeassistant.components.sensor.wink # homeassistant.components.sensor.wink
# homeassistant.components.switch.wink # homeassistant.components.switch.wink
python-wink==0.7.7 python-wink==0.7.8
# homeassistant.components.keyboard # homeassistant.components.keyboard
pyuserinput==0.1.9 pyuserinput==0.1.9
# homeassistant.components.vera # homeassistant.components.vera
pyvera==0.2.10 pyvera==0.2.12
# homeassistant.components.wemo # homeassistant.components.wemo
pywemo==0.4.3 pywemo==0.4.3
@ -410,7 +419,7 @@ vsure==0.8.1
wakeonlan==0.2.2 wakeonlan==0.2.2
# homeassistant.components.media_player.gpmdp # homeassistant.components.media_player.gpmdp
websocket-client==0.35.0 websocket-client==0.37.0
# homeassistant.components.zigbee # homeassistant.components.zigbee
xbee-helper==0.0.7 xbee-helper==0.0.7

View File

@ -1,10 +1,9 @@
flake8>=2.5.4 flake8>=2.6.0
pylint>=1.5.5 pylint>=1.5.6
coveralls>=1.1 coveralls>=1.1
pytest>=2.9.1 pytest>=2.9.2
pytest-cov>=2.2.0 pytest-cov>=2.2.1
pytest-timeout>=1.0.0 pytest-timeout>=1.0.0
pytest-capturelog>=0.7 pytest-capturelog>=0.7
betamax==0.7.0
pydocstyle>=1.0.0 pydocstyle>=1.0.0
httpretty==0.8.14 requests_mock>=1.0

View File

@ -17,7 +17,6 @@ REQUIRES = [
'pip>=7.0.0', 'pip>=7.0.0',
'jinja2>=2.8', 'jinja2>=2.8',
'voluptuous==0.8.9', 'voluptuous==0.8.9',
'eventlet==0.19.0',
] ]
setup( setup(

View File

@ -1,27 +1,25 @@
"""Test the initialization.""" """Setup some common test helper things."""
import betamax import functools
from homeassistant import util from homeassistant import util
from homeassistant.util import location from homeassistant.util import location
with betamax.Betamax.configure() as config:
config.cassette_library_dir = 'tests/cassettes'
# Automatically called during different setups. Too often forgotten def test_real(func):
# so mocked by default. """Force a function to require a keyword _test_real to be passed in."""
location.detect_location_info = lambda: location.LocationInfo( @functools.wraps(func)
ip='1.1.1.1', def guard_func(*args, **kwargs):
country_code='US', real = kwargs.pop('_test_real', None)
country_name='United States',
region_code='CA',
region_name='California',
city='San Diego',
zip_code='92122',
time_zone='America/Los_Angeles',
latitude='2.0',
longitude='1.0',
use_fahrenheit=True,
)
location.elevation = lambda latitude, longitude: 0 if not real:
raise Exception('Forgot to mock or pass "_test_real=True" to %s',
func.__name__)
return func(*args, **kwargs)
return guard_func
# Guard a few functions that would make network connections
location.detect_location_info = test_real(location.detect_location_info)
location.elevation = test_real(location.elevation)
util.get_local_ip = lambda: '127.0.0.1' util.get_local_ip = lambda: '127.0.0.1'

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -35,6 +35,7 @@ def get_test_home_assistant(num_threads=None):
hass.config.config_dir = get_test_config_dir() hass.config.config_dir = get_test_config_dir()
hass.config.latitude = 32.87336 hass.config.latitude = 32.87336
hass.config.longitude = -117.22743 hass.config.longitude = -117.22743
hass.config.elevation = 0
hass.config.time_zone = date_util.get_time_zone('US/Pacific') hass.config.time_zone = date_util.get_time_zone('US/Pacific')
hass.config.temperature_unit = TEMP_CELSIUS hass.config.temperature_unit = TEMP_CELSIUS
@ -105,6 +106,13 @@ def ensure_sun_set(hass):
fire_time_changed(hass, sun.next_setting_utc(hass) + timedelta(seconds=10)) fire_time_changed(hass, sun.next_setting_utc(hass) + timedelta(seconds=10))
def load_fixture(filename):
"""Helper to load a fixture."""
path = os.path.join(os.path.dirname(__file__), 'fixtures', filename)
with open(path) as fp:
return fp.read()
def mock_state_change_event(hass, new_state, old_state=None): def mock_state_change_event(hass, new_state, old_state=None):
"""Mock state change envent.""" """Mock state change envent."""
event_data = { event_data = {

View File

@ -1,8 +1,8 @@
"""The tests the for Locative device tracker platform.""" """The tests the for Locative device tracker platform."""
import time
import unittest import unittest
from unittest.mock import patch from unittest.mock import patch
import eventlet
import requests import requests
from homeassistant import bootstrap, const from homeassistant import bootstrap, const
@ -32,12 +32,9 @@ def setUpModule(): # pylint: disable=invalid-name
bootstrap.setup_component(hass, http.DOMAIN, { bootstrap.setup_component(hass, http.DOMAIN, {
http.DOMAIN: { http.DOMAIN: {
http.CONF_SERVER_PORT: SERVER_PORT http.CONF_SERVER_PORT: SERVER_PORT
} },
}) })
# Set up API
bootstrap.setup_component(hass, 'api')
# Set up device tracker # Set up device tracker
bootstrap.setup_component(hass, device_tracker.DOMAIN, { bootstrap.setup_component(hass, device_tracker.DOMAIN, {
device_tracker.DOMAIN: { device_tracker.DOMAIN: {
@ -46,7 +43,7 @@ def setUpModule(): # pylint: disable=invalid-name
}) })
hass.start() hass.start()
eventlet.sleep(0.05) time.sleep(0.05)
def tearDownModule(): # pylint: disable=invalid-name def tearDownModule(): # pylint: disable=invalid-name

View File

@ -1,17 +1,17 @@
"""The tests for the forecast.io platform.""" """The tests for the forecast.io platform."""
import json
import re import re
import os
import unittest import unittest
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
import forecastio import forecastio
import httpretty
from requests.exceptions import HTTPError from requests.exceptions import HTTPError
import requests_mock
from homeassistant.components.sensor import forecast from homeassistant.components.sensor import forecast
from homeassistant import core as ha from homeassistant import core as ha
from tests.common import load_fixture
class TestForecastSetup(unittest.TestCase): class TestForecastSetup(unittest.TestCase):
"""Test the forecast.io platform.""" """Test the forecast.io platform."""
@ -48,29 +48,14 @@ class TestForecastSetup(unittest.TestCase):
response = forecast.setup_platform(self.hass, self.config, MagicMock()) response = forecast.setup_platform(self.hass, self.config, MagicMock())
self.assertFalse(response) self.assertFalse(response)
@httpretty.activate @requests_mock.Mocker()
@patch('forecastio.api.get_forecast', wraps=forecastio.api.get_forecast) @patch('forecastio.api.get_forecast', wraps=forecastio.api.get_forecast)
def test_setup(self, mock_get_forecast): def test_setup(self, m, mock_get_forecast):
"""Test for successfully setting up the forecast.io platform.""" """Test for successfully setting up the forecast.io platform."""
def load_fixture_from_json(): uri = ('https://api.forecast.io\/forecast\/(\w+)\/'
cwd = os.path.dirname(__file__) '(-?\d+\.?\d*),(-?\d+\.?\d*)')
fixture_path = os.path.join(cwd, '..', 'fixtures', 'forecast.json') m.get(re.compile(uri),
with open(fixture_path) as file: text=load_fixture('forecast.json'))
content = json.load(file)
return json.dumps(content)
# Mock out any calls to the actual API and
# return the fixture json instead
uri = 'api.forecast.io\/forecast\/(\w+)\/(-?\d+\.?\d*),(-?\d+\.?\d*)'
httpretty.register_uri(
httpretty.GET,
re.compile(uri),
body=load_fixture_from_json(),
)
# The following will raise an error if the regex for the mock was
# incorrect and we actually try to go out to the internet.
httpretty.HTTPretty.allow_net_connect = False
forecast.setup_platform(self.hass, self.config, MagicMock()) forecast.setup_platform(self.hass, self.config, MagicMock())
self.assertTrue(mock_get_forecast.called) self.assertTrue(mock_get_forecast.called)
self.assertEqual(mock_get_forecast.call_count, 1) self.assertEqual(mock_get_forecast.call_count, 1)

View File

@ -1,39 +1,40 @@
"""The tests for the Yr sensor platform.""" """The tests for the Yr sensor platform."""
from datetime import datetime from datetime import datetime
from unittest import TestCase
from unittest.mock import patch from unittest.mock import patch
import pytest import requests_mock
from homeassistant.bootstrap import _setup_component from homeassistant.bootstrap import _setup_component
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from tests.common import get_test_home_assistant from tests.common import get_test_home_assistant, load_fixture
@pytest.mark.usefixtures('betamax_session') class TestSensorYr(TestCase):
class TestSensorYr:
"""Test the Yr sensor.""" """Test the Yr sensor."""
def setup_method(self, method): def setUp(self):
"""Setup things to be run when tests are started.""" """Setup things to be run when tests are started."""
self.hass = get_test_home_assistant() self.hass = get_test_home_assistant()
self.hass.config.latitude = 32.87336 self.hass.config.latitude = 32.87336
self.hass.config.longitude = 117.22743 self.hass.config.longitude = 117.22743
def teardown_method(self, method): def tearDown(self):
"""Stop everything that was started.""" """Stop everything that was started."""
self.hass.stop() self.hass.stop()
def test_default_setup(self, betamax_session): @requests_mock.Mocker()
def test_default_setup(self, m):
"""Test the default setup.""" """Test the default setup."""
m.get('http://api.yr.no/weatherapi/locationforecast/1.9/',
text=load_fixture('yr.no.json'))
now = datetime(2016, 6, 9, 1, tzinfo=dt_util.UTC) now = datetime(2016, 6, 9, 1, tzinfo=dt_util.UTC)
with patch('homeassistant.components.sensor.yr.requests.Session', with patch('homeassistant.components.sensor.yr.dt_util.utcnow',
return_value=betamax_session): return_value=now):
with patch('homeassistant.components.sensor.yr.dt_util.utcnow', assert _setup_component(self.hass, 'sensor', {
return_value=now): 'sensor': {'platform': 'yr',
assert _setup_component(self.hass, 'sensor', { 'elevation': 0}})
'sensor': {'platform': 'yr',
'elevation': 0}})
state = self.hass.states.get('sensor.yr_symbol') state = self.hass.states.get('sensor.yr_symbol')
@ -41,23 +42,24 @@ class TestSensorYr:
assert state.state.isnumeric() assert state.state.isnumeric()
assert state.attributes.get('unit_of_measurement') is None assert state.attributes.get('unit_of_measurement') is None
def test_custom_setup(self, betamax_session): @requests_mock.Mocker()
def test_custom_setup(self, m):
"""Test a custom setup.""" """Test a custom setup."""
m.get('http://api.yr.no/weatherapi/locationforecast/1.9/',
text=load_fixture('yr.no.json'))
now = datetime(2016, 6, 9, 1, tzinfo=dt_util.UTC) now = datetime(2016, 6, 9, 1, tzinfo=dt_util.UTC)
with patch('homeassistant.components.sensor.yr.requests.Session', with patch('homeassistant.components.sensor.yr.dt_util.utcnow',
return_value=betamax_session): return_value=now):
with patch('homeassistant.components.sensor.yr.dt_util.utcnow', assert _setup_component(self.hass, 'sensor', {
return_value=now): 'sensor': {'platform': 'yr',
assert _setup_component(self.hass, 'sensor', { 'elevation': 0,
'sensor': {'platform': 'yr', 'monitored_conditions': [
'elevation': 0, 'pressure',
'monitored_conditions': [ 'windDirection',
'pressure', 'humidity',
'windDirection', 'fog',
'humidity', 'windSpeed']}})
'fog',
'windSpeed']}})
state = self.hass.states.get('sensor.yr_pressure') state = self.hass.states.get('sensor.yr_pressure')
assert 'hPa' == state.attributes.get('unit_of_measurement') assert 'hPa' == state.attributes.get('unit_of_measurement')

View File

@ -1,9 +1,9 @@
"""The tests for the Alexa component.""" """The tests for the Alexa component."""
# pylint: disable=protected-access,too-many-public-methods # pylint: disable=protected-access,too-many-public-methods
import unittest
import json import json
import time
import unittest
import eventlet
import requests import requests
from homeassistant import bootstrap, const from homeassistant import bootstrap, const
@ -86,8 +86,7 @@ def setUpModule(): # pylint: disable=invalid-name
}) })
hass.start() hass.start()
time.sleep(0.05)
eventlet.sleep(0.1)
def tearDownModule(): # pylint: disable=invalid-name def tearDownModule(): # pylint: disable=invalid-name

View File

@ -1,12 +1,12 @@
"""The tests for the Home Assistant API component.""" """The tests for the Home Assistant API component."""
# pylint: disable=protected-access,too-many-public-methods # pylint: disable=protected-access,too-many-public-methods
# from contextlib import closing from contextlib import closing
import json import json
import tempfile import tempfile
import time
import unittest import unittest
from unittest.mock import patch from unittest.mock import patch
import eventlet
import requests import requests
from homeassistant import bootstrap, const from homeassistant import bootstrap, const
@ -48,10 +48,7 @@ def setUpModule(): # pylint: disable=invalid-name
bootstrap.setup_component(hass, 'api') bootstrap.setup_component(hass, 'api')
hass.start() hass.start()
time.sleep(0.05)
# To start HTTP
# TODO fix this
eventlet.sleep(0.05)
def tearDownModule(): # pylint: disable=invalid-name def tearDownModule(): # pylint: disable=invalid-name
@ -387,25 +384,23 @@ class TestAPI(unittest.TestCase):
headers=HA_HEADERS) headers=HA_HEADERS)
self.assertEqual(422, req.status_code) self.assertEqual(422, req.status_code)
# TODO disabled because eventlet cannot validate # Setup a real one
# a connection to itself, need a second instance req = requests.post(
# # Setup a real one _url(const.URL_API_EVENT_FORWARD),
# req = requests.post( data=json.dumps({
# _url(const.URL_API_EVENT_FORWARD), 'api_password': API_PASSWORD,
# data=json.dumps({ 'host': '127.0.0.1',
# 'api_password': API_PASSWORD, 'port': SERVER_PORT
# 'host': '127.0.0.1', }),
# 'port': SERVER_PORT headers=HA_HEADERS)
# }), self.assertEqual(200, req.status_code)
# headers=HA_HEADERS)
# self.assertEqual(200, req.status_code)
# # Delete it again.. # Delete it again..
# req = requests.delete( req = requests.delete(
# _url(const.URL_API_EVENT_FORWARD), _url(const.URL_API_EVENT_FORWARD),
# data=json.dumps({}), data=json.dumps({}),
# headers=HA_HEADERS) headers=HA_HEADERS)
# self.assertEqual(400, req.status_code) self.assertEqual(400, req.status_code)
req = requests.delete( req = requests.delete(
_url(const.URL_API_EVENT_FORWARD), _url(const.URL_API_EVENT_FORWARD),
@ -425,57 +420,58 @@ class TestAPI(unittest.TestCase):
headers=HA_HEADERS) headers=HA_HEADERS)
self.assertEqual(200, req.status_code) self.assertEqual(200, req.status_code)
# def test_stream(self): def test_stream(self):
# """Test the stream.""" """Test the stream."""
# listen_count = self._listen_count() listen_count = self._listen_count()
# with closing(requests.get(_url(const.URL_API_STREAM), timeout=3, with closing(requests.get(_url(const.URL_API_STREAM), timeout=3,
# stream=True, headers=HA_HEADERS)) as req: stream=True, headers=HA_HEADERS)) as req:
stream = req.iter_content(1)
self.assertEqual(listen_count + 1, self._listen_count())
# self.assertEqual(listen_count + 1, self._listen_count()) hass.bus.fire('test_event')
# hass.bus.fire('test_event') data = self._stream_next_event(stream)
# data = self._stream_next_event(req) self.assertEqual('test_event', data['event_type'])
# self.assertEqual('test_event', data['event_type']) def test_stream_with_restricted(self):
"""Test the stream with restrictions."""
listen_count = self._listen_count()
url = _url('{}?restrict=test_event1,test_event3'.format(
const.URL_API_STREAM))
with closing(requests.get(url, stream=True, timeout=3,
headers=HA_HEADERS)) as req:
stream = req.iter_content(1)
self.assertEqual(listen_count + 1, self._listen_count())
# def test_stream_with_restricted(self): hass.bus.fire('test_event1')
# """Test the stream with restrictions.""" data = self._stream_next_event(stream)
# listen_count = self._listen_count() self.assertEqual('test_event1', data['event_type'])
# url = _url('{}?restrict=test_event1,test_event3'.format(
# const.URL_API_STREAM))
# with closing(requests.get(url, stream=True, timeout=3,
# headers=HA_HEADERS)) as req:
# self.assertEqual(listen_count + 1, self._listen_count())
# hass.bus.fire('test_event1') hass.bus.fire('test_event2')
# data = self._stream_next_event(req) hass.bus.fire('test_event3')
# self.assertEqual('test_event1', data['event_type'])
# hass.bus.fire('test_event2') data = self._stream_next_event(stream)
# hass.bus.fire('test_event3') self.assertEqual('test_event3', data['event_type'])
# data = self._stream_next_event(req) def _stream_next_event(self, stream):
# self.assertEqual('test_event3', data['event_type']) """Read the stream for next event while ignoring ping."""
while True:
data = b''
last_new_line = False
for dat in stream:
if dat == b'\n' and last_new_line:
break
data += dat
last_new_line = dat == b'\n'
# def _stream_next_event(self, stream): conv = data.decode('utf-8').strip()[6:]
# """Read the stream for next event while ignoring ping."""
# while True:
# data = b''
# last_new_line = False
# for dat in stream.iter_content(1):
# if dat == b'\n' and last_new_line:
# break
# data += dat
# last_new_line = dat == b'\n'
# conv = data.decode('utf-8').strip()[6:] if conv != 'ping':
break
# if conv != 'ping': return json.loads(conv)
# break
# return json.loads(conv) def _listen_count(self):
"""Return number of event listeners."""
# def _listen_count(self): return sum(hass.bus.listeners.values())
# """Return number of event listeners."""
# return sum(hass.bus.listeners.values())

View File

@ -1,9 +1,9 @@
"""The tests for Home Assistant frontend.""" """The tests for Home Assistant frontend."""
# pylint: disable=protected-access,too-many-public-methods # pylint: disable=protected-access,too-many-public-methods
import re import re
import time
import unittest import unittest
import eventlet
import requests import requests
import homeassistant.bootstrap as bootstrap import homeassistant.bootstrap as bootstrap
@ -42,10 +42,7 @@ def setUpModule(): # pylint: disable=invalid-name
bootstrap.setup_component(hass, 'frontend') bootstrap.setup_component(hass, 'frontend')
hass.start() hass.start()
time.sleep(0.05)
# Give eventlet time to start
# TODO fix this
eventlet.sleep(0.05)
def tearDownModule(): # pylint: disable=invalid-name def tearDownModule(): # pylint: disable=invalid-name

View File

@ -1,8 +1,8 @@
"""The tests for the Home Assistant HTTP component.""" """The tests for the Home Assistant HTTP component."""
# pylint: disable=protected-access,too-many-public-methods # pylint: disable=protected-access,too-many-public-methods
import logging import logging
import time
import eventlet
import requests import requests
from homeassistant import bootstrap, const from homeassistant import bootstrap, const
@ -43,8 +43,7 @@ def setUpModule(): # pylint: disable=invalid-name
bootstrap.setup_component(hass, 'api') bootstrap.setup_component(hass, 'api')
hass.start() hass.start()
time.sleep(0.05)
eventlet.sleep(0.05)
def tearDownModule(): # pylint: disable=invalid-name def tearDownModule(): # pylint: disable=invalid-name
@ -83,7 +82,7 @@ class TestHttp:
logs = caplog.text() logs = caplog.text()
assert const.URL_API in logs # assert const.URL_API in logs
assert API_PASSWORD not in logs assert API_PASSWORD not in logs
def test_access_denied_with_wrong_password_in_url(self): def test_access_denied_with_wrong_password_in_url(self):
@ -106,5 +105,5 @@ class TestHttp:
logs = caplog.text() logs = caplog.text()
assert const.URL_API in logs # assert const.URL_API in logs
assert API_PASSWORD not in logs assert API_PASSWORD not in logs

View File

@ -131,7 +131,7 @@ class TestComponentsCore(unittest.TestCase):
assert state.attributes.get('hello') == 'world' assert state.attributes.get('hello') == 'world'
@patch('homeassistant.components._LOGGER.error') @patch('homeassistant.components._LOGGER.error')
@patch('homeassistant.bootstrap.process_ha_core_config') @patch('homeassistant.config.process_ha_core_config')
def test_reload_core_with_wrong_conf(self, mock_process, mock_error): def test_reload_core_with_wrong_conf(self, mock_process, mock_error):
"""Test reload core conf service.""" """Test reload core conf service."""
with TemporaryDirectory() as conf_dir: with TemporaryDirectory() as conf_dir:

13
tests/fixtures/freegeoip.io.json vendored Normal file
View File

@ -0,0 +1,13 @@
{
"ip": "1.2.3.4",
"country_code": "US",
"country_name": "United States",
"region_code": "CA",
"region_name": "California",
"city": "San Diego",
"zip_code": "92122",
"time_zone": "America\/Los_Angeles",
"latitude": 32.8594,
"longitude": -117.2073,
"metro_code": 825
}

View File

@ -0,0 +1,13 @@
{
"results" : [
{
"elevation" : 101.5,
"location" : {
"lat" : 32.54321,
"lng" : -117.12345
},
"resolution" : 4.8
}
],
"status" : "OK"
}

16
tests/fixtures/ip-api.com.json vendored Normal file
View File

@ -0,0 +1,16 @@
{
"as": "AS20001 Time Warner Cable Internet LLC",
"city": "San Diego",
"country": "United States",
"countryCode": "US",
"isp": "Time Warner Cable",
"lat": 32.8594,
"lon": -117.2073,
"org": "Time Warner Cable",
"query": "1.2.3.4",
"region": "CA",
"regionName": "California",
"status": "success",
"timezone": "America\/Los_Angeles",
"zip": "92122"
}

1184
tests/fixtures/yr.no.json vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,5 @@
"""Test the bootstrapping.""" """Test the bootstrapping."""
# pylint: disable=too-many-public-methods,protected-access # pylint: disable=too-many-public-methods,protected-access
import os
import tempfile import tempfile
from unittest import mock from unittest import mock
import threading import threading
@ -8,10 +7,7 @@ import threading
import voluptuous as vol import voluptuous as vol
from homeassistant import bootstrap, loader from homeassistant import bootstrap, loader
from homeassistant.const import (__version__, CONF_LATITUDE, CONF_LONGITUDE,
CONF_NAME, CONF_CUSTOMIZE)
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA from homeassistant.helpers.config_validation import PLATFORM_SCHEMA
from tests.common import get_test_home_assistant, MockModule, MockPlatform from tests.common import get_test_home_assistant, MockModule, MockPlatform
@ -24,23 +20,22 @@ class TestBootstrap:
def setup_method(self, method): def setup_method(self, method):
"""Setup the test.""" """Setup the test."""
self.backup_cache = loader._COMPONENT_CACHE
if method == self.test_from_config_file: if method == self.test_from_config_file:
return return
self.hass = get_test_home_assistant() self.hass = get_test_home_assistant()
self.backup_cache = loader._COMPONENT_CACHE
def teardown_method(self, method): def teardown_method(self, method):
"""Clean up.""" """Clean up."""
dt_util.DEFAULT_TIME_ZONE = ORIG_TIMEZONE dt_util.DEFAULT_TIME_ZONE = ORIG_TIMEZONE
if method == self.test_from_config_file:
return
self.hass.stop() self.hass.stop()
loader._COMPONENT_CACHE = self.backup_cache loader._COMPONENT_CACHE = self.backup_cache
def test_from_config_file(self): @mock.patch('homeassistant.util.location.detect_location_info',
return_value=None)
def test_from_config_file(self, mock_detect):
"""Test with configuration file.""" """Test with configuration file."""
components = ['browser', 'conversation', 'script'] components = ['browser', 'conversation', 'script']
with tempfile.NamedTemporaryFile() as fp: with tempfile.NamedTemporaryFile() as fp:
@ -48,71 +43,10 @@ class TestBootstrap:
fp.write('{}:\n'.format(comp).encode('utf-8')) fp.write('{}:\n'.format(comp).encode('utf-8'))
fp.flush() fp.flush()
hass = bootstrap.from_config_file(fp.name) self.hass = bootstrap.from_config_file(fp.name)
components.append('group') components.append('group')
assert sorted(components) == sorted(self.hass.config.components)
assert sorted(components) == sorted(hass.config.components)
def test_remove_lib_on_upgrade(self):
"""Test removal of library on upgrade."""
with tempfile.TemporaryDirectory() as config_dir:
version_path = os.path.join(config_dir, '.HA_VERSION')
lib_dir = os.path.join(config_dir, 'deps')
check_file = os.path.join(lib_dir, 'check')
with open(version_path, 'wt') as outp:
outp.write('0.7.0')
os.mkdir(lib_dir)
with open(check_file, 'w'):
pass
self.hass.config.config_dir = config_dir
assert os.path.isfile(check_file)
bootstrap.process_ha_config_upgrade(self.hass)
assert not os.path.isfile(check_file)
def test_not_remove_lib_if_not_upgrade(self):
"""Test removal of library with no upgrade."""
with tempfile.TemporaryDirectory() as config_dir:
version_path = os.path.join(config_dir, '.HA_VERSION')
lib_dir = os.path.join(config_dir, 'deps')
check_file = os.path.join(lib_dir, 'check')
with open(version_path, 'wt') as outp:
outp.write(__version__)
os.mkdir(lib_dir)
with open(check_file, 'w'):
pass
self.hass.config.config_dir = config_dir
bootstrap.process_ha_config_upgrade(self.hass)
assert os.path.isfile(check_file)
def test_entity_customization(self):
"""Test entity customization through configuration."""
config = {CONF_LATITUDE: 50,
CONF_LONGITUDE: 50,
CONF_NAME: 'Test',
CONF_CUSTOMIZE: {'test.test': {'hidden': True}}}
bootstrap.process_ha_core_config(self.hass, config)
entity = Entity()
entity.entity_id = 'test.test'
entity.hass = self.hass
entity.update_ha_state()
state = self.hass.states.get('test.test')
assert state.attributes['hidden']
def test_handle_setup_circular_dependency(self): def test_handle_setup_circular_dependency(self):
"""Test the setup of circular dependencies.""" """Test the setup of circular dependencies."""
@ -302,8 +236,7 @@ class TestBootstrap:
assert not bootstrap._setup_component(self.hass, 'comp', None) assert not bootstrap._setup_component(self.hass, 'comp', None)
assert 'comp' not in self.hass.config.components assert 'comp' not in self.hass.config.components
@mock.patch('homeassistant.bootstrap.process_ha_core_config') def test_home_assistant_core_config_validation(self):
def test_home_assistant_core_config_validation(self, mock_process):
"""Test if we pass in wrong information for HA conf.""" """Test if we pass in wrong information for HA conf."""
# Extensive HA conf validation testing is done in test_config.py # Extensive HA conf validation testing is done in test_config.py
assert None is bootstrap.from_config_dict({ assert None is bootstrap.from_config_dict({
@ -311,7 +244,6 @@ class TestBootstrap:
'latitude': 'some string' 'latitude': 'some string'
} }
}) })
assert not mock_process.called
def test_component_setup_with_validation_and_dependency(self): def test_component_setup_with_validation_and_dependency(self):
"""Test all config is passed to dependencies.""" """Test all config is passed to dependencies."""

View File

@ -1,22 +1,28 @@
"""Test config utils.""" """Test config utils."""
# pylint: disable=too-many-public-methods,protected-access # pylint: disable=too-many-public-methods,protected-access
import os
import tempfile
import unittest import unittest
import unittest.mock as mock import unittest.mock as mock
import os
import pytest import pytest
from voluptuous import MultipleInvalid from voluptuous import MultipleInvalid
from homeassistant.core import DOMAIN, HomeAssistantError from homeassistant.core import DOMAIN, HomeAssistantError, Config
import homeassistant.config as config_util import homeassistant.config as config_util
from homeassistant.const import ( from homeassistant.const import (
CONF_LATITUDE, CONF_LONGITUDE, CONF_TEMPERATURE_UNIT, CONF_NAME, CONF_LATITUDE, CONF_LONGITUDE, CONF_TEMPERATURE_UNIT, CONF_NAME,
CONF_TIME_ZONE) CONF_TIME_ZONE, CONF_ELEVATION, CONF_CUSTOMIZE, __version__,
TEMP_FAHRENHEIT)
from homeassistant.util import location as location_util, dt as dt_util
from homeassistant.helpers.entity import Entity
from tests.common import get_test_config_dir from tests.common import (
get_test_config_dir, get_test_home_assistant)
CONFIG_DIR = get_test_config_dir() CONFIG_DIR = get_test_config_dir()
YAML_PATH = os.path.join(CONFIG_DIR, config_util.YAML_CONFIG_FILE) YAML_PATH = os.path.join(CONFIG_DIR, config_util.YAML_CONFIG_FILE)
ORIG_TIMEZONE = dt_util.DEFAULT_TIME_ZONE
def create_file(path): def create_file(path):
@ -30,9 +36,14 @@ class TestConfig(unittest.TestCase):
def tearDown(self): # pylint: disable=invalid-name def tearDown(self): # pylint: disable=invalid-name
"""Clean up.""" """Clean up."""
dt_util.DEFAULT_TIME_ZONE = ORIG_TIMEZONE
if os.path.isfile(YAML_PATH): if os.path.isfile(YAML_PATH):
os.remove(YAML_PATH) os.remove(YAML_PATH)
if hasattr(self, 'hass'):
self.hass.stop()
def test_create_default_config(self): def test_create_default_config(self):
"""Test creation of default config.""" """Test creation of default config."""
config_util.create_default_config(CONFIG_DIR, False) config_util.create_default_config(CONFIG_DIR, False)
@ -108,8 +119,15 @@ class TestConfig(unittest.TestCase):
[('hello', 0), ('world', 1)], [('hello', 0), ('world', 1)],
list(config_util.load_yaml_config_file(YAML_PATH).items())) list(config_util.load_yaml_config_file(YAML_PATH).items()))
@mock.patch('homeassistant.util.location.detect_location_info',
return_value=location_util.LocationInfo(
'0.0.0.0', 'US', 'United States', 'CA', 'California',
'San Diego', '92122', 'America/Los_Angeles', 32.8594,
-117.2073, True))
@mock.patch('homeassistant.util.location.elevation', return_value=101)
@mock.patch('builtins.print') @mock.patch('builtins.print')
def test_create_default_config_detect_location(self, mock_print): def test_create_default_config_detect_location(self, mock_detect,
mock_elev, mock_print):
"""Test that detect location sets the correct config keys.""" """Test that detect location sets the correct config keys."""
config_util.ensure_config_exists(CONFIG_DIR) config_util.ensure_config_exists(CONFIG_DIR)
@ -120,15 +138,16 @@ class TestConfig(unittest.TestCase):
ha_conf = config[DOMAIN] ha_conf = config[DOMAIN]
expected_values = { expected_values = {
CONF_LATITUDE: 2.0, CONF_LATITUDE: 32.8594,
CONF_LONGITUDE: 1.0, CONF_LONGITUDE: -117.2073,
CONF_ELEVATION: 101,
CONF_TEMPERATURE_UNIT: 'F', CONF_TEMPERATURE_UNIT: 'F',
CONF_NAME: 'Home', CONF_NAME: 'Home',
CONF_TIME_ZONE: 'America/Los_Angeles' CONF_TIME_ZONE: 'America/Los_Angeles'
} }
self.assertEqual(expected_values, ha_conf) assert expected_values == ha_conf
self.assertTrue(mock_print.called) assert mock_print.called
@mock.patch('builtins.print') @mock.patch('builtins.print')
def test_create_default_config_returns_none_if_write_error(self, def test_create_default_config_returns_none_if_write_error(self,
@ -166,3 +185,127 @@ class TestConfig(unittest.TestCase):
}, },
}, },
}) })
def test_entity_customization(self):
"""Test entity customization through configuration."""
self.hass = get_test_home_assistant()
config = {CONF_LATITUDE: 50,
CONF_LONGITUDE: 50,
CONF_NAME: 'Test',
CONF_CUSTOMIZE: {'test.test': {'hidden': True}}}
config_util.process_ha_core_config(self.hass, config)
entity = Entity()
entity.entity_id = 'test.test'
entity.hass = self.hass
entity.update_ha_state()
state = self.hass.states.get('test.test')
assert state.attributes['hidden']
def test_remove_lib_on_upgrade(self):
"""Test removal of library on upgrade."""
with tempfile.TemporaryDirectory() as config_dir:
version_path = os.path.join(config_dir, '.HA_VERSION')
lib_dir = os.path.join(config_dir, 'deps')
check_file = os.path.join(lib_dir, 'check')
with open(version_path, 'wt') as outp:
outp.write('0.7.0')
os.mkdir(lib_dir)
with open(check_file, 'w'):
pass
self.hass = get_test_home_assistant()
self.hass.config.config_dir = config_dir
assert os.path.isfile(check_file)
config_util.process_ha_config_upgrade(self.hass)
assert not os.path.isfile(check_file)
def test_not_remove_lib_if_not_upgrade(self):
"""Test removal of library with no upgrade."""
with tempfile.TemporaryDirectory() as config_dir:
version_path = os.path.join(config_dir, '.HA_VERSION')
lib_dir = os.path.join(config_dir, 'deps')
check_file = os.path.join(lib_dir, 'check')
with open(version_path, 'wt') as outp:
outp.write(__version__)
os.mkdir(lib_dir)
with open(check_file, 'w'):
pass
self.hass = get_test_home_assistant()
self.hass.config.config_dir = config_dir
config_util.process_ha_config_upgrade(self.hass)
assert os.path.isfile(check_file)
def test_loading_configuration(self):
"""Test loading core config onto hass object."""
config = Config()
hass = mock.Mock(config=config)
config_util.process_ha_core_config(hass, {
'latitude': 60,
'longitude': 50,
'elevation': 25,
'name': 'Huis',
'temperature_unit': 'F',
'time_zone': 'America/New_York',
})
assert config.latitude == 60
assert config.longitude == 50
assert config.elevation == 25
assert config.location_name == 'Huis'
assert config.temperature_unit == TEMP_FAHRENHEIT
assert config.time_zone.zone == 'America/New_York'
@mock.patch('homeassistant.util.location.detect_location_info',
return_value=location_util.LocationInfo(
'0.0.0.0', 'US', 'United States', 'CA', 'California',
'San Diego', '92122', 'America/Los_Angeles', 32.8594,
-117.2073, True))
@mock.patch('homeassistant.util.location.elevation', return_value=101)
def test_discovering_configuration(self, mock_detect, mock_elevation):
"""Test auto discovery for missing core configs."""
config = Config()
hass = mock.Mock(config=config)
config_util.process_ha_core_config(hass, {})
assert config.latitude == 32.8594
assert config.longitude == -117.2073
assert config.elevation == 101
assert config.location_name == 'San Diego'
assert config.temperature_unit == TEMP_FAHRENHEIT
assert config.time_zone.zone == 'America/Los_Angeles'
@mock.patch('homeassistant.util.location.detect_location_info',
return_value=None)
@mock.patch('homeassistant.util.location.elevation', return_value=0)
def test_discovering_configuration_auto_detect_fails(self, mock_detect,
mock_elevation):
"""Test config remains unchanged if discovery fails."""
config = Config()
hass = mock.Mock(config=config)
config_util.process_ha_core_config(hass, {})
blankConfig = Config()
assert config.latitude == blankConfig.latitude
assert config.longitude == blankConfig.longitude
assert config.elevation == blankConfig.elevation
assert config.location_name == blankConfig.location_name
assert config.temperature_unit == blankConfig.temperature_unit
assert config.time_zone == blankConfig.time_zone

View File

@ -1,9 +1,8 @@
"""Test Home Assistant remote methods and classes.""" """Test Home Assistant remote methods and classes."""
# pylint: disable=protected-access,too-many-public-methods # pylint: disable=protected-access,too-many-public-methods
import time
import unittest import unittest
import eventlet
import homeassistant.core as ha import homeassistant.core as ha
import homeassistant.bootstrap as bootstrap import homeassistant.bootstrap as bootstrap
import homeassistant.remote as remote import homeassistant.remote as remote
@ -47,10 +46,7 @@ def setUpModule(): # pylint: disable=invalid-name
bootstrap.setup_component(hass, 'api') bootstrap.setup_component(hass, 'api')
hass.start() hass.start()
time.sleep(0.05)
# Give eventlet time to start
# TODO fix this
eventlet.sleep(0.05)
master_api = remote.API("127.0.0.1", API_PASSWORD, MASTER_PORT) master_api = remote.API("127.0.0.1", API_PASSWORD, MASTER_PORT)
@ -63,10 +59,6 @@ def setUpModule(): # pylint: disable=invalid-name
slave.start() slave.start()
# Give eventlet time to start
# TODO fix this
eventlet.sleep(0.05)
def tearDownModule(): # pylint: disable=invalid-name def tearDownModule(): # pylint: disable=invalid-name
"""Stop the Home Assistant server and slave.""" """Stop the Home Assistant server and slave."""
@ -257,7 +249,6 @@ class TestRemoteClasses(unittest.TestCase):
slave.pool.block_till_done() slave.pool.block_till_done()
# Wait till master gives updated state # Wait till master gives updated state
hass.pool.block_till_done() hass.pool.block_till_done()
eventlet.sleep(0.01)
self.assertEqual("remote.statemachine test", self.assertEqual("remote.statemachine test",
slave.states.get("remote.test").state) slave.states.get("remote.test").state)
@ -266,13 +257,11 @@ class TestRemoteClasses(unittest.TestCase):
"""Remove statemachine from master.""" """Remove statemachine from master."""
hass.states.set("remote.master_remove", "remove me!") hass.states.set("remote.master_remove", "remove me!")
hass.pool.block_till_done() hass.pool.block_till_done()
eventlet.sleep(0.01)
self.assertIn('remote.master_remove', slave.states.entity_ids()) self.assertIn('remote.master_remove', slave.states.entity_ids())
hass.states.remove("remote.master_remove") hass.states.remove("remote.master_remove")
hass.pool.block_till_done() hass.pool.block_till_done()
eventlet.sleep(0.01)
self.assertNotIn('remote.master_remove', slave.states.entity_ids()) self.assertNotIn('remote.master_remove', slave.states.entity_ids())
@ -280,14 +269,12 @@ class TestRemoteClasses(unittest.TestCase):
"""Remove statemachine from slave.""" """Remove statemachine from slave."""
hass.states.set("remote.slave_remove", "remove me!") hass.states.set("remote.slave_remove", "remove me!")
hass.pool.block_till_done() hass.pool.block_till_done()
eventlet.sleep(0.01)
self.assertIn('remote.slave_remove', slave.states.entity_ids()) self.assertIn('remote.slave_remove', slave.states.entity_ids())
self.assertTrue(slave.states.remove("remote.slave_remove")) self.assertTrue(slave.states.remove("remote.slave_remove"))
slave.pool.block_till_done() slave.pool.block_till_done()
hass.pool.block_till_done() hass.pool.block_till_done()
eventlet.sleep(0.01)
self.assertNotIn('remote.slave_remove', slave.states.entity_ids()) self.assertNotIn('remote.slave_remove', slave.states.entity_ids())
@ -306,6 +293,5 @@ class TestRemoteClasses(unittest.TestCase):
slave.pool.block_till_done() slave.pool.block_till_done()
# Wait till master gives updated event # Wait till master gives updated event
hass.pool.block_till_done() hass.pool.block_till_done()
eventlet.sleep(0.01)
self.assertEqual(1, len(test_value)) self.assertEqual(1, len(test_value))

View File

@ -1,9 +1,15 @@
"""Test Home Assistant location util methods.""" """Test Home Assistant location util methods."""
# pylint: disable=too-many-public-methods # pylint: disable=too-many-public-methods
import unittest from unittest import TestCase
from unittest.mock import patch
import requests
import requests_mock
import homeassistant.util.location as location_util import homeassistant.util.location as location_util
from tests.common import load_fixture
# Paris # Paris
COORDINATES_PARIS = (48.864716, 2.349014) COORDINATES_PARIS = (48.864716, 2.349014)
# New York # New York
@ -20,26 +26,124 @@ DISTANCE_KM = 5846.39
DISTANCE_MILES = 3632.78 DISTANCE_MILES = 3632.78
class TestLocationUtil(unittest.TestCase): class TestLocationUtil(TestCase):
"""Test util location methods.""" """Test util location methods."""
def test_get_distance_to_same_place(self):
"""Test getting the distance."""
meters = location_util.distance(COORDINATES_PARIS[0],
COORDINATES_PARIS[1],
COORDINATES_PARIS[0],
COORDINATES_PARIS[1])
assert meters == 0
def test_get_distance(self): def test_get_distance(self):
"""Test getting the distance.""" """Test getting the distance."""
meters = location_util.distance(COORDINATES_PARIS[0], meters = location_util.distance(COORDINATES_PARIS[0],
COORDINATES_PARIS[1], COORDINATES_PARIS[1],
COORDINATES_NEW_YORK[0], COORDINATES_NEW_YORK[0],
COORDINATES_NEW_YORK[1]) COORDINATES_NEW_YORK[1])
self.assertAlmostEqual(meters / 1000, DISTANCE_KM, places=2)
assert meters/1000 - DISTANCE_KM < 0.01
def test_get_kilometers(self): def test_get_kilometers(self):
"""Test getting the distance between given coordinates in km.""" """Test getting the distance between given coordinates in km."""
kilometers = location_util.vincenty(COORDINATES_PARIS, kilometers = location_util.vincenty(COORDINATES_PARIS,
COORDINATES_NEW_YORK) COORDINATES_NEW_YORK)
self.assertEqual(round(kilometers, 2), DISTANCE_KM) assert round(kilometers, 2) == DISTANCE_KM
def test_get_miles(self): def test_get_miles(self):
"""Test getting the distance between given coordinates in miles.""" """Test getting the distance between given coordinates in miles."""
miles = location_util.vincenty(COORDINATES_PARIS, miles = location_util.vincenty(COORDINATES_PARIS,
COORDINATES_NEW_YORK, COORDINATES_NEW_YORK,
miles=True) miles=True)
self.assertEqual(round(miles, 2), DISTANCE_MILES) assert round(miles, 2) == DISTANCE_MILES
@requests_mock.Mocker()
def test_detect_location_info_freegeoip(self, m):
"""Test detect location info using freegeoip."""
m.get(location_util.FREEGEO_API,
text=load_fixture('freegeoip.io.json'))
info = location_util.detect_location_info(_test_real=True)
assert info is not None
assert info.ip == '1.2.3.4'
assert info.country_code == 'US'
assert info.country_name == 'United States'
assert info.region_code == 'CA'
assert info.region_name == 'California'
assert info.city == 'San Diego'
assert info.zip_code == '92122'
assert info.time_zone == 'America/Los_Angeles'
assert info.latitude == 32.8594
assert info.longitude == -117.2073
assert info.use_fahrenheit
@requests_mock.Mocker()
@patch('homeassistant.util.location._get_freegeoip', return_value=None)
def test_detect_location_info_ipapi(self, mock_req, mock_freegeoip):
"""Test detect location info using freegeoip."""
mock_req.get(location_util.IP_API,
text=load_fixture('ip-api.com.json'))
info = location_util.detect_location_info(_test_real=True)
assert info is not None
assert info.ip == '1.2.3.4'
assert info.country_code == 'US'
assert info.country_name == 'United States'
assert info.region_code == 'CA'
assert info.region_name == 'California'
assert info.city == 'San Diego'
assert info.zip_code == '92122'
assert info.time_zone == 'America/Los_Angeles'
assert info.latitude == 32.8594
assert info.longitude == -117.2073
assert info.use_fahrenheit
@patch('homeassistant.util.location.elevation', return_value=0)
@patch('homeassistant.util.location._get_freegeoip', return_value=None)
@patch('homeassistant.util.location._get_ip_api', return_value=None)
def test_detect_location_info_both_queries_fail(self, mock_ipapi,
mock_freegeoip,
mock_elevation):
"""Ensure we return None if both queries fail."""
info = location_util.detect_location_info(_test_real=True)
assert info is None
@patch('homeassistant.util.location.requests.get',
side_effect=requests.RequestException)
def test_freegeoip_query_raises(self, mock_get):
"""Test freegeoip query when the request to API fails."""
info = location_util._get_freegeoip()
assert info is None
@patch('homeassistant.util.location.requests.get',
side_effect=requests.RequestException)
def test_ip_api_query_raises(self, mock_get):
"""Test ip api query when the request to API fails."""
info = location_util._get_ip_api()
assert info is None
@patch('homeassistant.util.location.requests.get',
side_effect=requests.RequestException)
def test_elevation_query_raises(self, mock_get):
"""Test elevation when the request to API fails."""
elevation = location_util.elevation(10, 10, _test_real=True)
assert elevation == 0
@requests_mock.Mocker()
def test_elevation_query_fails(self, mock_req):
"""Test elevation when the request to API fails."""
mock_req.get(location_util.ELEVATION_URL, text='{}', status_code=401)
elevation = location_util.elevation(10, 10, _test_real=True)
assert elevation == 0
@requests_mock.Mocker()
def test_elevation_query_nonjson(self, mock_req):
"""Test if elevation API returns a non JSON value."""
mock_req.get(location_util.ELEVATION_URL, text='{ I am not JSON }')
elevation = location_util.elevation(10, 10, _test_real=True)
assert elevation == 0

View File

@ -49,7 +49,7 @@ class TestPackageUtil(unittest.TestCase):
self.assertTrue(package.check_package_exists( self.assertTrue(package.check_package_exists(
TEST_NEW_REQ, self.lib_dir)) TEST_NEW_REQ, self.lib_dir))
bootstrap.mount_local_lib_path(self.tmp_dir.name) bootstrap._mount_local_lib_path(self.tmp_dir.name)
try: try:
import pyhelloworld3 import pyhelloworld3