diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index ff7e73a00f1..8b3d3ee6f23 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -3,7 +3,6 @@ import logging import logging.handlers import os -import shutil import sys from collections import defaultdict from threading import RLock @@ -12,21 +11,15 @@ import voluptuous as vol import homeassistant.components as core_components 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.helpers.config_validation as cv 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 -from homeassistant.const import ( - 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.const import EVENT_COMPONENT_LOADED, PLATFORM_FORMAT from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import ( - event_decorators, service, config_per_platform, extract_domain_configs, - entity) + event_decorators, service, config_per_platform, extract_domain_configs) _LOGGER = logging.getLogger(__name__) _SETUP_LOCK = RLock() @@ -208,11 +201,6 @@ def prepare_setup_platform(hass, config, domain, platform_name): 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 def from_config_dict(config, hass=None, config_dir=None, enable_log=True, 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: config_dir = os.path.abspath(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, {}) try: - process_ha_core_config(hass, config_util.CORE_CONFIG_SCHEMA( - core_config)) - except vol.MultipleInvalid as ex: + conf_util.process_ha_core_config(hass, core_config) + except vol.Invalid as ex: cv.log_exception(_LOGGER, ex, 'homeassistant', core_config) return None - process_ha_config_upgrade(hass) + conf_util.process_ha_config_upgrade(hass) if enable_log: 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 config_dir = os.path.abspath(os.path.dirname(config_path)) 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) try: - config_dict = config_util.load_yaml_config_file(config_path) + config_dict = conf_util.load_yaml_config_file(config_path) except HomeAssistantError: 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) -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): """Ensure Home Assistant loader is prepared.""" if not loader.PREPARED: 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')) diff --git a/homeassistant/components/__init__.py b/homeassistant/components/__init__.py index d625f9cd3cd..38780ed9b28 100644 --- a/homeassistant/components/__init__.py +++ b/homeassistant/components/__init__.py @@ -121,16 +121,16 @@ def setup(hass, config): def handle_reload_config(call): """Service handler for reloading core config.""" from homeassistant.exceptions import HomeAssistantError - from homeassistant import config, bootstrap + from homeassistant import config as conf_util try: - path = config.find_config_file(hass.config.config_dir) - conf = config.load_yaml_config_file(path) + path = conf_util.find_config_file(hass.config.config_dir) + conf = conf_util.load_yaml_config_file(path) except HomeAssistantError as err: _LOGGER.error(err) 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, handle_reload_config) diff --git a/homeassistant/components/api.py b/homeassistant/components/api.py index b538a62d008..f0073bad838 100644 --- a/homeassistant/components/api.py +++ b/homeassistant/components/api.py @@ -6,7 +6,7 @@ https://home-assistant.io/developers/api/ """ import json import logging -from time import time +import queue import homeassistant.core as ha import homeassistant.remote as rem @@ -72,19 +72,14 @@ class APIEventStream(HomeAssistantView): def get(self, request): """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() + to_write = queue.Queue() restrict = request.args.get('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.""" if event.event_type == EVENT_TIME_CHANGED: return @@ -99,28 +94,20 @@ class APIEventStream(HomeAssistantView): else: data = json.dumps(event, cls=rem.JSONEncoder) - cur_hub.schedule_call_global(0, lambda: to_write.put(data)) + to_write.put(data) def stream(): """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)) - last_msg = time() # Fire off one message right away to have browsers fire open event to_write.put(STREAM_PING_PAYLOAD) while True: try: - # Somehow our queue.get sometimes takes too long to - # 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) + payload = to_write.get(timeout=STREAM_PING_INTERVAL) if payload is stop_obj: break @@ -129,15 +116,13 @@ class APIEventStream(HomeAssistantView): _LOGGER.debug('STREAM %s WRITING %s', id(stop_obj), msg.strip()) yield msg.encode("UTF-8") - last_msg = time() - except Empty: - if time() - last_msg > 50: - to_write.put(STREAM_PING_PAYLOAD) + except queue.Empty: + to_write.put(STREAM_PING_PAYLOAD) except GeneratorExit: - _LOGGER.debug('STREAM %s RESPONSE CLOSED', id(stop_obj)) 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') diff --git a/homeassistant/components/binary_sensor/homematic.py b/homeassistant/components/binary_sensor/homematic.py index 08ea2099445..8e874079ee6 100644 --- a/homeassistant/components/binary_sensor/homematic.py +++ b/homeassistant/components/binary_sensor/homematic.py @@ -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 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: "" # e.g. "JEQ0XXXXXXX" - name: "" (optional) - - -Configuration (multiple channels, like motion detector with buttons): - -binary_sensor: - - platform: homematic - address: "" # e.g. "JEQ0XXXXXXX" - param: (device-dependent) (optional) - button: n (integer of channel to map, device-dependent) (optional) - name: "" (optional) -binary_sensor: - - platform: homematic - ... """ - import logging from homeassistant.const import STATE_UNKNOWN from homeassistant.components.binary_sensor import BinarySensorDevice @@ -47,44 +23,25 @@ SENSOR_TYPES_CLASS = { "RemoteMotion": None } -SUPPORT_HM_EVENT_AS_BINMOD = [ - "PRESS_LONG", - "PRESS_SHORT" -] - def setup_platform(hass, config, add_callback_devices, discovery_info=None): - """Setup the platform.""" - if discovery_info: - return homematic.setup_hmdevice_discovery_helper(HMBinarySensor, - discovery_info, - add_callback_devices) - # Manual - return homematic.setup_hmdevice_entity_helper(HMBinarySensor, - config, - add_callback_devices) + """Setup the Homematic binary sensor platform.""" + if discovery_info is None: + return + + return homematic.setup_hmdevice_discovery_helper(HMBinarySensor, + discovery_info, + add_callback_devices) class HMBinarySensor(homematic.HMDevice, BinarySensorDevice): - """Represents diverse binary Homematic units in Home Assistant.""" + """Representation of a binary Homematic device.""" @property def is_on(self): - """Return True if switch is on.""" + """Return true if switch is on.""" if not self.available: 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()) @property @@ -107,63 +64,37 @@ class HMBinarySensor(homematic.HMDevice, BinarySensorDevice): if not super()._check_hm_to_ha_object(): 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): - _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 - # load possible binary sensor - available_bin = self._create_binary_list_from_hm() - # if exists user value? - if self._state and self._state not in available_bin: - _LOGGER.critical("This %s have no binary with %s!", self._name, + if self._state and self._state not in self._hmdevice.BINARYNODE: + _LOGGER.critical("This %s have no binary with %s", self._name, self._state) return False - # only check and give a warining to User - if self._state is None and len(available_bin) > 1: - _LOGGER.warning("%s have multible binary params. It use all " + - "binary nodes as one. Possible param values: %s", - self._name, str(available_bin)) + # only check and give a warning to the user + if self._state is None and len(self._hmdevice.BINARYNODE) > 1: + _LOGGER.critical("%s have multiple binary params. It use all " + "binary nodes as one. Possible param values: %s", + self._name, str(self._hmdevice.BINARYNODE)) + return False return True 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() - # load possible binary sensor - available_bin = self._create_binary_list_from_hm() - # object have 1 binary - if self._state is None and len(available_bin) == 1: - for value in available_bin: + if self._state is None and len(self._hmdevice.BINARYNODE) == 1: + for value in self._hmdevice.BINARYNODE: 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 if self._state: _LOGGER.debug("%s init datastruct with main node '%s'", self._name, self._state) 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 diff --git a/homeassistant/components/binary_sensor/wink.py b/homeassistant/components/binary_sensor/wink.py index d9c2b7d577a..9ec85e63503 100644 --- a/homeassistant/components/binary_sensor/wink.py +++ b/homeassistant/components/binary_sensor/wink.py @@ -7,10 +7,12 @@ at https://home-assistant.io/components/sensor.wink/ import logging 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.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 SENSOR_TYPES = { @@ -41,14 +43,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices([WinkBinarySensorDevice(sensor)]) -class WinkBinarySensorDevice(BinarySensorDevice, Entity): +class WinkBinarySensorDevice(WinkDevice, BinarySensorDevice, Entity): """Representation of a Wink sensor.""" def __init__(self, wink): """Initialize the Wink binary sensor.""" - self.wink = wink + super().__init__(wink) + wink = get_component('wink') self._unit_of_measurement = self.wink.UNIT - self._battery = self.wink.battery_level self.capability = self.wink.capability() @property @@ -67,35 +69,3 @@ class WinkBinarySensorDevice(BinarySensorDevice, Entity): def sensor_class(self): """Return the class of this sensor, from SENSOR_CLASSES.""" 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 diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 87342528987..2f23118a1c3 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -6,6 +6,7 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/camera/ """ import logging +import time from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent @@ -81,8 +82,6 @@ class Camera(Entity): def mjpeg_stream(self, response): """Generate an HTTP MJPEG stream from camera images.""" - import eventlet - def stream(): """Stream images as mjpeg stream.""" try: @@ -99,7 +98,7 @@ class Camera(Entity): last_image = img_bytes - eventlet.sleep(0.5) + time.sleep(0.5) except GeneratorExit: pass diff --git a/homeassistant/components/camera/rpi_camera.py b/homeassistant/components/camera/rpi_camera.py index cda48d1ddfa..ee67d097286 100644 --- a/homeassistant/components/camera/rpi_camera.py +++ b/homeassistant/components/camera/rpi_camera.py @@ -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 subprocess import logging @@ -43,7 +47,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class RaspberryCamera(Camera): - """Raspberry Pi camera.""" + """Representation of a Raspberry Pi camera.""" def __init__(self, device_info): """Initialize Raspberry Pi camera component.""" diff --git a/homeassistant/components/garage_door/wink.py b/homeassistant/components/garage_door/wink.py index 18ec6f2ba56..73692290f50 100644 --- a/homeassistant/components/garage_door/wink.py +++ b/homeassistant/components/garage_door/wink.py @@ -7,9 +7,10 @@ https://home-assistant.io/components/garage_door.wink/ import logging 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): @@ -31,38 +32,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None): pywink.get_garage_doors()) -class WinkGarageDoorDevice(GarageDoorDevice): +class WinkGarageDoorDevice(WinkDevice, GarageDoorDevice): """Representation of a Wink garage door.""" def __init__(self, wink): """Initialize the garage door.""" - self.wink = 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() + WinkDevice.__init__(self, wink) @property def is_closed(self): """Return true if door is closed.""" return self.wink.state() == 0 - @property - def available(self): - """True if connection == True.""" - return self.wink.available - def close_door(self): """Close the door.""" self.wink.set_state(0) @@ -70,16 +51,3 @@ class WinkGarageDoorDevice(GarageDoorDevice): def open_door(self): """Open the door.""" 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 diff --git a/homeassistant/components/homematic.py b/homeassistant/components/homematic.py index 7b3e265a9dd..fb31408bd82 100644 --- a/homeassistant/components/homematic.py +++ b/homeassistant/components/homematic.py @@ -1,17 +1,8 @@ """ -Support for Homematic Devices. +Support for Homematic devices. For more details about this component, please refer to the documentation at https://home-assistant.io/components/homematic/ - -Configuration: - -homematic: - local_ip: "" - local_port: - remote_ip: "" - remote_port: - autodetect: "" (optional, experimental, detect all devices) """ import time import logging @@ -21,11 +12,10 @@ from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity DOMAIN = 'homematic' -REQUIREMENTS = ['pyhomematic==0.1.6'] +REQUIREMENTS = ['pyhomematic==0.1.8'] HOMEMATIC = None HOMEMATIC_LINK_DELAY = 0.5 -HOMEMATIC_DEVICES = {} DISCOVER_SWITCHES = "homematic.switch" DISCOVER_LIGHTS = "homematic.light" @@ -35,18 +25,22 @@ DISCOVER_ROLLERSHUTTER = "homematic.rollershutter" DISCOVER_THERMOSTATS = "homematic.thermostat" 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 = { DISCOVER_SWITCHES: ["Switch", "SwitchPowermeter"], DISCOVER_LIGHTS: ["Dimmer"], DISCOVER_SENSORS: ["SwitchPowermeter", "Motion", "MotionV2", "RemoteMotion", "ThermostatWall", "AreaThermostat", - "RotaryHandleSensor"], + "RotaryHandleSensor", "WaterSensor"], DISCOVER_THERMOSTATS: ["Thermostat", "ThermostatWall", "MAXThermostat"], - DISCOVER_BINARY_SENSORS: ["Remote", "ShutterContact", "Smoke", "SmokeV2", - "Motion", "MotionV2", "RemoteMotion", - "GongSensor"], + DISCOVER_BINARY_SENSORS: ["ShutterContact", "Smoke", "SmokeV2", + "Motion", "MotionV2", "RemoteMotion"], DISCOVER_ROLLERSHUTTER: ["Blind"] } @@ -66,6 +60,13 @@ HM_ATTRIBUTE_SUPPORT = { "VOLTAGE": ["Voltage", {}] } +HM_PRESS_EVENTS = [ + "PRESS_SHORT", + "PRESS_LONG", + "PRESS_CONT", + "PRESS_LONG_RELEASE" +] + _LOGGER = logging.getLogger(__name__) @@ -81,6 +82,8 @@ def setup(hass, config): remote_ip = config[DOMAIN].get("remote_ip", None) remote_port = config[DOMAIN].get("remote_port", 2001) 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) if remote_ip is None or local_ip is None: @@ -89,12 +92,15 @@ def setup(hass, config): # Create server thread bound_system_callback = partial(system_callback_handler, hass, config) + # pylint: disable=unexpected-keyword-arg HOMEMATIC = HMConnection(local=local_ip, localport=local_port, remote=remote_ip, remoteport=remote_port, systemcallback=bound_system_callback, resolvenames=resolvenames, + rpcusername=username, + rpcpassword=password, interface_id="homeassistant") # 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: key_dict[dev['ADDRESS'].split(':')[0]] = True - # Connect devices already created in HA to pyhomematic and - # add remaining devices to list - devices_not_created = [] + # Register EVENTS + # Search all device with a EVENTNODE that include data + bound_event_callback = partial(_hm_event_handler, hass) for dev in key_dict: - if dev in HOMEMATIC_DEVICES: - for hm_element in HOMEMATIC_DEVICES[dev]: - hm_element.link_homematic() - else: - devices_not_created.append(dev) + if dev not in HOMEMATIC.devices: + continue + + hmdevice = HOMEMATIC.devices.get(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, # all devices not configured are added. - autodetect = config[DOMAIN].get("autodetect", False) - _LOGGER.debug("Autodetect is %s / unknown device: %s", str(autodetect), - str(devices_not_created)) - if autodetect and devices_not_created: + if key_dict: for component_name, discovery_type in ( ('switch', DISCOVER_SWITCHES), ('light', DISCOVER_LIGHTS), @@ -143,8 +150,7 @@ def system_callback_handler(hass, config, src, *args): ('sensor', DISCOVER_SENSORS), ('thermostat', DISCOVER_THERMOSTATS)): # Get all devices of a specific type - found_devices = _get_devices(discovery_type, - devices_not_created) + found_devices = _get_devices(discovery_type, key_dict) # When devices of this type are found # 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): - """Get devices.""" - from homeassistant.components.binary_sensor.homematic import \ - SUPPORT_HM_EVENT_AS_BINMOD - + """Get the Homematic devices.""" # run device_arr = [] - if not keys: - keys = HOMEMATIC.devices for key in keys: device = HOMEMATIC.devices[key] - if device.__class__.__name__ not in HM_DEVICE_TYPES[device_type]: - continue + class_name = device.__class__.__name__ 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 if device_type == DISCOVER_SENSORS: metadata.update(device.SENSORNODE) elif device_type == DISCOVER_BINARY_SENSORS: metadata.update(device.BINARYNODE) - # Also add supported events as binary 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) + params = _create_params_list(device, metadata, device_type) if params: # Generate options for 1...n elements with 1...n params for channel in range(1, device.ELEMENT + 1): @@ -194,9 +193,9 @@ def _get_devices(device_type, keys): device_dict = dict(platform="homematic", address=key, name=name, - button=channel) + channel=channel) if param is not None: - device_dict["param"] = param + device_dict[ATTR_PARAM] = param # Add new device device_arr.append(device_dict) @@ -209,15 +208,22 @@ def _get_devices(device_type, keys): 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.""" 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 for channel in range(1, hmdevice.ELEMENT + 1): param_chan = [] - try: - for node, meta_chan in metadata.items(): + for node, meta_chan in metadata.items(): + try: # Is this attribute ignored? if node in HM_IGNORE_DISCOVERY_NODE: continue @@ -227,15 +233,17 @@ def _create_params_list(hmdevice, metadata): elif channel == 1: # First channel can have other data channel param_chan.append(node) - # pylint: disable=broad-except - except Exception as err: - _LOGGER.error("Exception generating %s (%s): %s", - hmdevice.ADDRESS, str(metadata), str(err)) - # Default parameter - if not param_chan: + except (TypeError, ValueError): + _LOGGER.error("Exception generating %s (%s)", + hmdevice.ADDRESS, str(metadata)) + + # default parameter is merge is off + if len(param_chan) == 0 and not merge: param_chan.append(None) + # 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, str(params)) @@ -264,55 +272,55 @@ def _create_ha_name(name, channel, param): def setup_hmdevice_discovery_helper(hmdevicetype, discovery_info, add_callback_devices): """Helper to setup Homematic devices with discovery info.""" - for config in discovery_info["devices"]: - ret = setup_hmdevice_entity_helper(hmdevicetype, config, - add_callback_devices) - if not ret: - _LOGGER.error("Setup discovery error with config %s", str(config)) + for config in discovery_info[ATTR_DISCOVER_DEVICES]: + _LOGGER.debug("Add device %s from config: %s", + str(hmdevicetype), 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 -def setup_hmdevice_entity_helper(hmdevicetype, config, add_callback_devices): - """Helper to setup Homematic devices.""" - if HOMEMATIC is None: - _LOGGER.error('Error setting up HMDevice: Server not configured.') - return False +def _hm_event_handler(hass, device, caller, attribute, value): + """Handle all pyhomematic device events.""" + channel = device.split(":")[1] + address = device.split(":")[0] + hmdevice = HOMEMATIC.devices.get(address) - address = config.get('address', None) - if address is None: - _LOGGER.error("Error setting up device '%s': " + - "'address' missing in configuration.", address) - return False + # is not a event? + if attribute not in hmdevice.EVENTNODE: + return - _LOGGER.debug("Add device %s from config: %s", - str(hmdevicetype), str(config)) - # 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) + _LOGGER.debug("Event %s for %s channel %s", attribute, + hmdevice.NAME, channel) - # Add to HA - add_callback_devices([new_device]) + # a keypress event + 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 - if address in HOMEMATIC.devices: - return new_device.link_homematic() - return True + _LOGGER.warning("Event is unknown and not forwarded to HA") class HMDevice(Entity): - """Homematic device base object.""" + """The Homematic device base object.""" # pylint: disable=too-many-instance-attributes def __init__(self, config): - """Initialize generic HM device.""" - self._name = config.get("name", None) - self._address = config.get("address", None) - self._channel = config.get("button", 1) - self._state = config.get("param", None) - self._hidden = config.get("hidden", False) + """Initialize a generic Homematic device.""" + self._name = config.get(ATTR_NAME, None) + self._address = config.get(ATTR_ADDRESS, None) + self._channel = config.get(ATTR_CHANNEL, 1) + self._state = config.get(ATTR_PARAM, None) self._data = {} self._hmdevice = None self._connected = False @@ -330,7 +338,7 @@ class HMDevice(Entity): @property 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 @property @@ -340,24 +348,23 @@ class HMDevice(Entity): @property 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 @property def available(self): - """Return True if device is available.""" + """Return true if device is available.""" return self._available - @property - def hidden(self): - """Return True if the entity should be hidden from UIs.""" - return self._hidden - @property def device_state_attributes(self): """Return device specific state attributes.""" attr = {} + # no data available to create + if not self.available: + return attr + # Generate an attributes list for node, data in HM_ATTRIBUTE_SUPPORT.items(): # 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]) attr[data[0]] = value + # static attributes + attr["ID"] = self._hmdevice.ADDRESS + return attr def link_homematic(self): - """Connect to homematic.""" + """Connect to Homematic.""" # device is already linked if self._connected: return True @@ -379,7 +389,7 @@ class HMDevice(Entity): self._hmdevice = HOMEMATIC.devices[self._address] 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) if self._check_hm_to_ha_object(): try: @@ -402,7 +412,7 @@ class HMDevice(Entity): _LOGGER.error("Exception while linking %s: %s", self._address, str(err)) else: - _LOGGER.critical("Delink %s object from HM!", self._name) + _LOGGER.critical("Delink %s object from HM", self._name) self._connected = False # Update HA @@ -429,18 +439,12 @@ class HMDevice(Entity): self._available = bool(value) have_change = True - # If it has changed, update HA + # If it has changed data point, update HA if have_change: _LOGGER.debug("%s update_ha_state after '%s'", self._name, attribute) 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): """Subscribe all required events to handle job.""" channels_to_sub = {} @@ -488,24 +492,21 @@ class HMDevice(Entity): if node in self._data: 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 def _hm_set_state(self, value): + """Set data to main datapoint.""" if self._state in self._data: self._data[self._state] = value def _hm_get_state(self): + """Get data from main datapoint.""" if self._state in self._data: return self._data[self._state] return None 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! """ @@ -521,7 +522,7 @@ class HMDevice(Entity): return True 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! """ diff --git a/homeassistant/components/http.py b/homeassistant/components/http.py index 1f77aac5ad4..11aa18cad5c 100644 --- a/homeassistant/components/http.py +++ b/homeassistant/components/http.py @@ -13,19 +13,19 @@ import re import ssl import voluptuous as vol -import homeassistant.core as ha import homeassistant.remote as rem from homeassistant import util from homeassistant.const import ( SERVER_PORT, HTTP_HEADER_HA_AUTH, HTTP_HEADER_CACHE_CONTROL, 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 import homeassistant.util.dt as dt_util import homeassistant.helpers.config_validation as cv 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_SERVER_HOST = "server_host" @@ -40,7 +40,8 @@ DATA_API_PASSWORD = 'api_password' # TLS configuation follows the best-practice guidelines # specified here: https://wiki.mozilla.org/Security/Server_Side_TLS # 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:" \ "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:" \ "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:" \ @@ -117,11 +118,17 @@ def setup(hass, config): cors_origins=cors_origins ) - hass.bus.listen_once( - ha.EVENT_HOMEASSISTANT_START, - lambda event: - threading.Thread(target=server.start, daemon=True, - name='WSGI-server').start()) + def start_wsgi_server(event): + """Start the WSGI server.""" + 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.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.cors_origins = cors_origins self.event_forwarder = None + self.server = None def register_view(self, view): """Register a view with the WSGI server. @@ -307,15 +315,34 @@ class HomeAssistantWSGI(object): def start(self): """Start the wsgi server.""" - from eventlet import wsgi - import eventlet + from cherrypy import wsgiserver + 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: - sock = eventlet.wrap_ssl(sock, certfile=self.ssl_certificate, - keyfile=self.ssl_key, server_side=True, - ssl_version=SSL_VERSION, ciphers=CIPHERS) - wsgi.server(sock, self, log=_LOGGER) + context = ssl.SSLContext(SSL_VERSION) + context.options |= SSL_OPTS + context.set_ciphers(CIPHERS) + 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): """Handle incoming request.""" @@ -362,6 +389,10 @@ class HomeAssistantWSGI(object): """Handle a request for base app + extra apps.""" 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) # Strip out any cachebusting MD5 fingerprints fingerprinted = _FINGERPRINT.match(environ.get('PATH_INFO', '')) diff --git a/homeassistant/components/influxdb.py b/homeassistant/components/influxdb.py index e9ae7de81bc..311d3fe83df 100644 --- a/homeassistant/components/influxdb.py +++ b/homeassistant/components/influxdb.py @@ -23,7 +23,7 @@ DEFAULT_DATABASE = 'home_assistant' DEFAULT_SSL = False DEFAULT_VERIFY_SSL = False -REQUIREMENTS = ['influxdb==2.12.0'] +REQUIREMENTS = ['influxdb==3.0.0'] CONF_HOST = 'host' CONF_PORT = 'port' diff --git a/homeassistant/components/light/enocean.py b/homeassistant/components/light/enocean.py index adb10a20fda..2c9db86e662 100644 --- a/homeassistant/components/light/enocean.py +++ b/homeassistant/components/light/enocean.py @@ -4,7 +4,6 @@ Support for EnOcean light sources. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.enocean/ """ - import logging import math @@ -86,7 +85,7 @@ class EnOceanLight(enocean.EnOceanDevice, Light): self._on_state = False 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._on_state = bool(val != 0) self.update_ha_state() diff --git a/homeassistant/components/light/homematic.py b/homeassistant/components/light/homematic.py index 159f3e4dbdc..b7e0328a574 100644 --- a/homeassistant/components/light/homematic.py +++ b/homeassistant/components/light/homematic.py @@ -1,21 +1,9 @@ """ -The homematic light platform. +Support for Homematic lighs. For more details about this platform, please refer to the documentation at 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: # e.g. "JEQ0XXXXXXX" - name: (optional) - button: n (integer of channel to map, device-dependent) """ - import logging from homeassistant.components.light import (ATTR_BRIGHTNESS, Light) from homeassistant.const import STATE_UNKNOWN @@ -23,24 +11,21 @@ import homeassistant.components.homematic as homematic _LOGGER = logging.getLogger(__name__) -# List of component names (string) your component depends upon. DEPENDENCIES = ['homematic'] def setup_platform(hass, config, add_callback_devices, discovery_info=None): - """Setup the platform.""" - if discovery_info: - return homematic.setup_hmdevice_discovery_helper(HMLight, - discovery_info, - add_callback_devices) - # Manual - return homematic.setup_hmdevice_entity_helper(HMLight, - config, - add_callback_devices) + """Setup the Homematic light platform.""" + if discovery_info is None: + return + + return homematic.setup_hmdevice_discovery_helper(HMLight, + discovery_info, + add_callback_devices) class HMLight(homematic.HMDevice, Light): - """Represents a Homematic Light in Home Assistant.""" + """Representation of a Homematic light.""" @property def brightness(self): @@ -55,7 +40,7 @@ class HMLight(homematic.HMDevice, Light): @property def is_on(self): - """Return True if light is on.""" + """Return true if light is on.""" try: return self._hm_get_state() > 0 except TypeError: @@ -78,24 +63,24 @@ class HMLight(homematic.HMDevice, Light): self._hmdevice.off(self._channel) 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 # Check compatibility from HMDevice if not super()._check_hm_to_ha_object(): 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): return True if isinstance(self._hmdevice, Dimmer): 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 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 super()._init_data_struct() diff --git a/homeassistant/components/light/osramlightify.py b/homeassistant/components/light/osramlightify.py index 33c759b21d5..243d11116da 100644 --- a/homeassistant/components/light/osramlightify.py +++ b/homeassistant/components/light/osramlightify.py @@ -1,19 +1,9 @@ """ Support for Osram Lightify. -Uses: https://github.com/aneumeier/python-lightify for the Osram light -interface. - -In order to use the platform just add the following to the configuration.yaml: - -light: - platform: osramlightify - host: - -Todo: -Add support for Non RGBW lights. +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/light.osramlightify/ """ - import logging import socket 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): - """Find and return lights.""" + """Setup Osram Lightify lights.""" import lightify host = config.get(CONF_HOST) if host: @@ -85,7 +75,7 @@ def setup_bridge(bridge, add_devices_callback): class OsramLightifyLight(Light): - """Defines an Osram Lightify Light.""" + """Representation of an Osram Lightify Light.""" def __init__(self, light_id, light, update_lights): """Initialize the light.""" diff --git a/homeassistant/components/light/wink.py b/homeassistant/components/light/wink.py index 2438cdaab9a..5fdec96f5d4 100644 --- a/homeassistant/components/light/wink.py +++ b/homeassistant/components/light/wink.py @@ -8,12 +8,13 @@ import logging from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, \ Light, ATTR_RGB_COLOR +from homeassistant.components.wink import WinkDevice from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.util import color as color_util from homeassistant.util.color import \ 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): @@ -35,26 +36,12 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): WinkLight(light) for light in pywink.get_bulbs()) -class WinkLight(Light): +class WinkLight(WinkDevice, Light): """Representation of a Wink light.""" def __init__(self, wink): - """ - Initialize the light. - - :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() + """Initialize the Wink device.""" + WinkDevice.__init__(self, wink) @property def is_on(self): @@ -66,11 +53,6 @@ class WinkLight(Light): """Return the brightness of the light.""" return int(self.wink.brightness() * 255) - @property - def available(self): - """True if connection == True.""" - return self.wink.available - @property def xy_color(self): """Current bulb color in CIE 1931 (XY) color space.""" @@ -112,7 +94,3 @@ class WinkLight(Light): def turn_off(self): """Turn the switch off.""" self.wink.set_state(False) - - def update(self): - """Update state of the light.""" - self.wink.update_state(require_desired_state_fulfilled=True) diff --git a/homeassistant/components/light/zwave.py b/homeassistant/components/light/zwave.py index b4aaf5e2b4f..7c9cb72db26 100644 --- a/homeassistant/components/light/zwave.py +++ b/homeassistant/components/light/zwave.py @@ -4,12 +4,31 @@ Support for Z-Wave lights. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.zwave/ """ +import logging + # Because we do not compile openzwave on CI # pylint: disable=import-error 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.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): @@ -28,7 +47,17 @@ def setup_platform(hass, config, add_devices, discovery_info=None): return 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): @@ -49,8 +78,9 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light): from pydispatch import dispatcher zwave.ZWaveDeviceEntity.__init__(self, value, DOMAIN) - - self._brightness, self._state = brightness_state(value) + self._brightness = None + self._state = None + self.update_properties() # Used for value change event handling self._refreshing = False @@ -59,6 +89,11 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light): dispatcher.connect( 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): """Called when a value has changed on the network.""" if self._value.value_id != value.value_id: @@ -66,7 +101,7 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light): if self._refreshing: self._refreshing = False - self._brightness, self._state = brightness_state(value) + self.update_properties() else: def _refresh_value(): """Used timer callback for delayed value refresh.""" @@ -107,3 +142,168 @@ class ZwaveDimmer(zwave.ZWaveDeviceEntity, Light): """Turn the device off.""" if self._value.node.set_dimmer(self._value.value_id, 0): 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) diff --git a/homeassistant/components/lock/vera.py b/homeassistant/components/lock/vera.py new file mode 100644 index 00000000000..f10b8857499 --- /dev/null +++ b/homeassistant/components/lock/vera.py @@ -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) diff --git a/homeassistant/components/lock/wink.py b/homeassistant/components/lock/wink.py index 2572796df35..7551302499a 100644 --- a/homeassistant/components/lock/wink.py +++ b/homeassistant/components/lock/wink.py @@ -7,9 +7,10 @@ https://home-assistant.io/components/lock.wink/ import logging 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): @@ -30,38 +31,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(WinkLockDevice(lock) for lock in pywink.get_locks()) -class WinkLockDevice(LockDevice): +class WinkLockDevice(WinkDevice, LockDevice): """Representation of a Wink lock.""" def __init__(self, wink): """Initialize the lock.""" - self.wink = 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() + WinkDevice.__init__(self, wink) @property def is_locked(self): """Return true if device is locked.""" return self.wink.state() - @property - def available(self): - """True if connection == True.""" - return self.wink.available - def lock(self, **kwargs): """Lock the device.""" self.wink.set_state(True) @@ -69,16 +50,3 @@ class WinkLockDevice(LockDevice): def unlock(self, **kwargs): """Unlock the device.""" 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 diff --git a/homeassistant/components/media_player/braviatv.py b/homeassistant/components/media_player/braviatv.py index ea316f57425..ef5f7516827 100644 --- a/homeassistant/components/media_player/braviatv.py +++ b/homeassistant/components/media_player/braviatv.py @@ -1,10 +1,8 @@ """ Support for interface with a Sony Bravia TV. -By Antonio Parraga Navarro - -dedicated to Isabel - +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/media_player.braviatv/ """ import logging import os @@ -38,6 +36,7 @@ SUPPORT_BRAVIA = SUPPORT_PAUSE | SUPPORT_VOLUME_STEP | \ def _get_mac_address(ip_address): + """Get the MAC address of the device.""" from subprocess import Popen, 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): - """Small configuration file management function.""" + """Create the configuration from a file.""" if config: # We're writing configuration 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 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) name = config.get(CONF_NAME) if name is None: @@ -176,7 +175,7 @@ class BraviaTVDevice(MediaPlayerDevice): """Representation of a Sony Bravia TV.""" def __init__(self, host, mac, name, pin): - """Initialize the sony bravia device.""" + """Initialize the Sony Bravia device.""" from braviarc import braviarc self._pin = pin diff --git a/homeassistant/components/media_player/cmus.py b/homeassistant/components/media_player/cmus.py index 43ddee3ba02..4726a1fa6a9 100644 --- a/homeassistant/components/media_player/cmus.py +++ b/homeassistant/components/media_player/cmus.py @@ -2,9 +2,8 @@ Support for interacting with and controlling the cmus music player. 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 from homeassistant.components.media_player import ( @@ -17,7 +16,7 @@ from homeassistant.const import (STATE_OFF, STATE_PAUSED, STATE_PLAYING, CONF_PORT) _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_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): - """Setup the Cmus platform.""" + """Setup the CMUS platform.""" from pycmus import exceptions host = config.get(CONF_HOST, None) @@ -44,7 +43,7 @@ def setup_platform(hass, config, add_devices, discover_info=None): class CmusDevice(MediaPlayerDevice): - """Representation of a running cmus.""" + """Representation of a running CMUS.""" # pylint: disable=no-member, too-many-public-methods, abstract-method def __init__(self, server, password, port, name): diff --git a/homeassistant/components/media_player/gpmdp.py b/homeassistant/components/media_player/gpmdp.py index 8259d043cf3..eb6e15379d8 100644 --- a/homeassistant/components/media_player/gpmdp.py +++ b/homeassistant/components/media_player/gpmdp.py @@ -15,7 +15,7 @@ from homeassistant.const import ( STATE_PLAYING, STATE_PAUSED, STATE_OFF) _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 diff --git a/homeassistant/components/media_player/kodi.py b/homeassistant/components/media_player/kodi.py index 432ff73c367..2a14af969fb 100644 --- a/homeassistant/components/media_player/kodi.py +++ b/homeassistant/components/media_player/kodi.py @@ -15,7 +15,7 @@ from homeassistant.const import ( STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING) _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_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK | \ diff --git a/homeassistant/components/media_player/roku.py b/homeassistant/components/media_player/roku.py index cd16dc4a620..6ff1ae1510f 100644 --- a/homeassistant/components/media_player/roku.py +++ b/homeassistant/components/media_player/roku.py @@ -4,7 +4,6 @@ Support for the roku media player. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.roku/ """ - import logging from homeassistant.components.media_player import ( @@ -77,7 +76,8 @@ class RokuDevice(MediaPlayerDevice): self.current_app = self.roku.current_app else: self.current_app = None - except requests.exceptions.ConnectionError: + except (requests.exceptions.ConnectionError, + requests.exceptions.ReadTimeout): self.current_app = None def get_source_list(self): diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml index d1ef92ee4d5..9ab831bdbb4 100644 --- a/homeassistant/components/media_player/services.yaml +++ b/homeassistant/components/media_player/services.yaml @@ -154,12 +154,20 @@ sonos_group_players: description: Name(s) of entites that will coordinate the grouping. Platform dependent. 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: description: Take a snapshot of the media player. fields: 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' sonos_restore: @@ -167,5 +175,5 @@ sonos_restore: fields: entity_id: - description: Name(s) of entites that will coordinate the grouping. Platform dependent. - example: 'media_player.living_room_sonos' \ No newline at end of file + description: Name(s) of entites that will be restored. Platform dependent. + example: 'media_player.living_room_sonos' diff --git a/homeassistant/components/media_player/snapcast.py b/homeassistant/components/media_player/snapcast.py index 44cdd414da4..998490fb9b9 100644 --- a/homeassistant/components/media_player/snapcast.py +++ b/homeassistant/components/media_player/snapcast.py @@ -4,7 +4,6 @@ Support for interacting with Snapcast clients. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.snapcast/ """ - import logging import socket diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index 9239f1edae8..7d0cd12175a 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -34,11 +34,12 @@ SUPPORT_SONOS = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE |\ SUPPORT_SEEK SERVICE_GROUP_PLAYERS = 'sonos_group_players' +SERVICE_UNJOIN = 'sonos_unjoin' SERVICE_SNAPSHOT = 'sonos_snapshot' 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): """Setup the Sonos platform.""" import soco @@ -72,47 +73,35 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(devices) _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): """Group media players, use player as coordinator.""" - entity_id = service.data.get('entity_id') + _apply_service(service, SonosDevice.group_players) - if entity_id: - _devices = [device for device in devices - if device.entity_id == entity_id] - else: - _devices = devices + def unjoin_service(service): + """Unjoin the player from a group.""" + _apply_service(service, SonosDevice.unjoin) - for device in _devices: - device.group_players() - device.update_ha_state(True) - - def snapshot(service): + def snapshot_service(service): """Take a snapshot.""" - entity_id = service.data.get('entity_id') + _apply_service(service, SonosDevice.snapshot) - if entity_id: - _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): + def restore_service(service): """Restore a snapshot.""" - 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: - device.restore(service) - device.update_ha_state(True) + _apply_service(service, SonosDevice.restore) descriptions = load_yaml_config_file( path.join(path.dirname(__file__), 'services.yaml')) @@ -121,12 +110,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None): group_players_service, descriptions.get(SERVICE_GROUP_PLAYERS)) + hass.services.register(DOMAIN, SERVICE_UNJOIN, + unjoin_service, + descriptions.get(SERVICE_UNJOIN)) + hass.services.register(DOMAIN, SERVICE_SNAPSHOT, - snapshot, + snapshot_service, descriptions.get(SERVICE_SNAPSHOT)) hass.services.register(DOMAIN, SERVICE_RESTORE, - restore, + restore_service, descriptions.get(SERVICE_RESTORE)) return True @@ -356,12 +349,17 @@ class SonosDevice(MediaPlayerDevice): self._player.partymode() @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.""" self.soco_snapshot.snapshot() @only_if_coordinator - def restore(self, service): + def restore(self): """Restore snapshot for the player.""" self.soco_snapshot.restore(True) diff --git a/homeassistant/components/media_player/universal.py b/homeassistant/components/media_player/universal.py index f5fa8cc486c..8bfdeebf85d 100644 --- a/homeassistant/components/media_player/universal.py +++ b/homeassistant/components/media_player/universal.py @@ -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 https://home-assistant.io/components/media_player.universal/ """ - import logging # pylint: disable=import-error from copy import copy diff --git a/homeassistant/components/rollershutter/homematic.py b/homeassistant/components/rollershutter/homematic.py index 737a7eb017d..9bdad7ee68c 100644 --- a/homeassistant/components/rollershutter/homematic.py +++ b/homeassistant/components/rollershutter/homematic.py @@ -6,13 +6,6 @@ https://home-assistant.io/components/rollershutter.homematic/ Important: For this platform to work the homematic component has to be properly configured. - -Configuration: - -rollershutter: - - platform: homematic - address: "" # e.g. "JEQ0XXXXXXX" - name: "" (optional) """ import logging @@ -29,14 +22,12 @@ DEPENDENCIES = ['homematic'] def setup_platform(hass, config, add_callback_devices, discovery_info=None): """Setup the platform.""" - if discovery_info: - return homematic.setup_hmdevice_discovery_helper(HMRollershutter, - discovery_info, - add_callback_devices) - # Manual - return homematic.setup_hmdevice_entity_helper(HMRollershutter, - config, - add_callback_devices) + if discovery_info is None: + return + + return homematic.setup_hmdevice_discovery_helper(HMRollershutter, + discovery_info, + add_callback_devices) class HMRollershutter(homematic.HMDevice, RollershutterDevice): diff --git a/homeassistant/components/rollershutter/wink.py b/homeassistant/components/rollershutter/wink.py index e01b2573ac6..8a31148da01 100644 --- a/homeassistant/components/rollershutter/wink.py +++ b/homeassistant/components/rollershutter/wink.py @@ -7,9 +7,10 @@ https://home-assistant.io/components/rollershutter.wink/ import logging from homeassistant.components.rollershutter import RollershutterDevice +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): @@ -31,38 +32,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None): pywink.get_shades()) -class WinkRollershutterDevice(RollershutterDevice): +class WinkRollershutterDevice(WinkDevice, RollershutterDevice): """Representation of a Wink rollershutter (shades).""" def __init__(self, wink): """Initialize the rollershutter.""" - self.wink = wink - self._battery = None + WinkDevice.__init__(self, wink) @property def should_poll(self): """Wink Shades don't track their position.""" 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): """Close the shade.""" self.wink.set_state(0) diff --git a/homeassistant/components/sensor/deutsche_bahn.py b/homeassistant/components/sensor/deutsche_bahn.py index a38ee76b3bb..99b96b971c9 100644 --- a/homeassistant/components/sensor/deutsche_bahn.py +++ b/homeassistant/components/sensor/deutsche_bahn.py @@ -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 https://home-assistant.io/components/sensor.deutsche_bahn/ """ 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.helpers.entity import Entity -_LOGGER = logging.getLogger(__name__) REQUIREMENTS = ['schiene==0.17'] + +CONF_START = 'from' +CONF_DESTINATION = 'to' 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. 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.""" - start = config.get('from') - goal = config.get('to') + start = config.get(CONF_START) + destination = config.get(CONF_DESTINATION) - if start is None: - _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) + add_devices([DeutscheBahnSensor(start, destination)]) # pylint: disable=too-few-public-methods @@ -63,16 +68,17 @@ class DeutscheBahnSensor(Entity): @property def state_attributes(self): """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): """Get the latest delay from bahn.de and updates the state.""" self.data.update() self._state = self.data.connections[0].get('departure', 'Unknown') if self.data.connections[0]['delay'] != 0: - self._state += " + {}".format( - self.data.connections[0]['delay'] - ) + self._state += " + {}".format(self.data.connections[0]['delay']) # pylint: disable=too-few-public-methods @@ -90,18 +96,15 @@ class SchieneData(object): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Update the connection data.""" - self.connections = self.schiene.connections(self.start, - self.goal, - datetime.now()) + self.connections = self.schiene.connections(self.start, self.goal) + for con in self.connections: - # Details info is not useful. - # Having a more consistent interface simplifies - # usage of Template sensors later on + # Detail info is not useful. Having a more consistent interface + # simplifies usage of template sensors. if 'details' in con: con.pop('details') - delay = con.get('delay', - {'delay_departure': 0, - 'delay_arrival': 0}) - # IMHO only delay_departure is usefull + delay = con.get('delay', {'delay_departure': 0, + 'delay_arrival': 0}) + # IMHO only delay_departure is useful con['delay'] = delay['delay_departure'] con['ontime'] = con.get('ontime', False) diff --git a/homeassistant/components/sensor/homematic.py b/homeassistant/components/sensor/homematic.py index f6f3825199b..2efa4fdef38 100644 --- a/homeassistant/components/sensor/homematic.py +++ b/homeassistant/components/sensor/homematic.py @@ -6,14 +6,6 @@ https://home-assistant.io/components/sensor.homematic/ Important: For this platform to work the homematic component has to be properly configured. - -Configuration: - -sensor: - - platform: homematic - address: # e.g. "JEQ0XXXXXXX" - name: (optional) - param: (optional) """ import logging @@ -41,14 +33,12 @@ HM_UNIT_HA_CAST = { def setup_platform(hass, config, add_callback_devices, discovery_info=None): """Setup the platform.""" - if discovery_info: - return homematic.setup_hmdevice_discovery_helper(HMSensor, - discovery_info, - add_callback_devices) - # Manual - return homematic.setup_hmdevice_entity_helper(HMSensor, - config, - add_callback_devices) + if discovery_info is None: + return + + return homematic.setup_hmdevice_discovery_helper(HMSensor, + discovery_info, + add_callback_devices) class HMSensor(homematic.HMDevice): diff --git a/homeassistant/components/sensor/onewire.py b/homeassistant/components/sensor/onewire.py index 6941fc952a6..a2a3f0811f2 100644 --- a/homeassistant/components/sensor/onewire.py +++ b/homeassistant/components/sensor/onewire.py @@ -12,21 +12,24 @@ from glob import glob from homeassistant.const import STATE_UNKNOWN, TEMP_CELSIUS 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__) # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """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('Check if dtoverlay=w1-gpio,gpiopin=4.') _LOGGER.error('is in your /boot/config.txt and') @@ -34,7 +37,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): return devs = [] - names = SENSOR_IDS + names = sensor_ids for key in config.keys(): if key == "names": @@ -47,9 +50,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): # map names to ids. elif isinstance(config['names'], dict): names = [] - for sensor_id in SENSOR_IDS: + for sensor_id in sensor_ids: 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)) add_devices(devs) @@ -88,14 +91,27 @@ class OneWire(Entity): def update(self): """Get the latest data from the device.""" - lines = self._read_temp_raw() - while lines[0].strip()[-3:] != 'YES': - time.sleep(0.2) + temp = -99 + if self._device_file.startswith('/sys/bus/w1/devices'): lines = self._read_temp_raw() - equals_pos = lines[1].find('t=') - if equals_pos != -1: - temp_string = lines[1][equals_pos+2:] - temp = round(float(temp_string) / 1000.0, 1) - if temp < -55 or temp > 125: - return - self._state = temp + while lines[0].strip()[-3:] != 'YES': + time.sleep(0.2) + lines = self._read_temp_raw() + equals_pos = lines[1].find('t=') + if equals_pos != -1: + temp_string = lines[1][equals_pos+2:] + 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 diff --git a/homeassistant/components/sensor/openexchangerates.py b/homeassistant/components/sensor/openexchangerates.py index f95e5c36233..920dfc46a90 100644 --- a/homeassistant/components/sensor/openexchangerates.py +++ b/homeassistant/components/sensor/openexchangerates.py @@ -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 import logging import requests @@ -41,7 +46,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class OpenexchangeratesSensor(Entity): - """Implementing the Openexchangerates sensor.""" + """Representation of an Openexchangerates sensor.""" def __init__(self, rest, name, quote): """Initialize the sensor.""" @@ -87,7 +92,7 @@ class OpenexchangeratesData(object): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): - """Get the latest data from openexchangerates.""" + """Get the latest data from openexchangerates.org.""" try: result = requests.get(self._resource, params={'base': self._base, 'app_id': diff --git a/homeassistant/components/sensor/thinkingcleaner.py b/homeassistant/components/sensor/thinkingcleaner.py index 1ba8593650e..f956ec5037f 100644 --- a/homeassistant/components/sensor/thinkingcleaner.py +++ b/homeassistant/components/sensor/thinkingcleaner.py @@ -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 from datetime import timedelta diff --git a/homeassistant/components/sensor/wink.py b/homeassistant/components/sensor/wink.py index 3fb914d6cd9..ac885152a2e 100644 --- a/homeassistant/components/sensor/wink.py +++ b/homeassistant/components/sensor/wink.py @@ -7,11 +7,12 @@ at https://home-assistant.io/components/sensor.wink/ import logging from homeassistant.const import (CONF_ACCESS_TOKEN, STATE_CLOSED, - STATE_OPEN, TEMP_CELSIUS, - ATTR_BATTERY_LEVEL) + STATE_OPEN, TEMP_CELSIUS) 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'] @@ -38,14 +39,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(WinkEggMinder(eggtray) for eggtray in pywink.get_eggtrays()) -class WinkSensorDevice(Entity): +class WinkSensorDevice(WinkDevice, Entity): """Representation of a Wink sensor.""" def __init__(self, wink): - """Initialize the sensor.""" - self.wink = wink + """Initialize the Wink device.""" + super().__init__(wink) + wink = get_component('wink') self.capability = self.wink.capability() - self._battery = self.wink.battery_level if self.wink.UNIT == "°": self._unit_of_measurement = TEMP_CELSIUS else: @@ -55,9 +56,9 @@ class WinkSensorDevice(Entity): def state(self): """Return the state.""" if self.capability == "humidity": - return self.wink.humidity_percentage() + return round(self.wink.humidity_percentage()) elif self.capability == "temperature": - return self.wink.temperature_float() + return round(self.wink.temperature_float(), 1) else: 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 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 def is_open(self): """Return true if door is open.""" 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 - def _battery_level(self): - """Return the battery level.""" - return self.wink.battery_level * 100 - - -class WinkEggMinder(Entity): +class WinkEggMinder(WinkDevice, Entity): """Representation of a Wink Egg Minder.""" def __init__(self, wink): """Initialize the sensor.""" - self.wink = wink - self._battery = self.wink.battery_level + WinkDevice.__init__(self, wink) @property def state(self): """Return the 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 diff --git a/homeassistant/components/sensor/yr.py b/homeassistant/components/sensor/yr.py index 725043c4da8..ddfbc68d974 100644 --- a/homeassistant/components/sensor/yr.py +++ b/homeassistant/components/sensor/yr.py @@ -15,7 +15,6 @@ from homeassistant.const import ( ) from homeassistant.helpers.entity import Entity from homeassistant.util import dt as dt_util -from homeassistant.util import location _LOGGER = logging.getLogger(__name__) @@ -54,16 +53,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Yr.no sensor.""" latitude = config.get(CONF_LATITUDE, hass.config.latitude) 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): _LOGGER.error("Latitude or longitude not set in Home Assistant config") return False - if elevation is None: - elevation = location.elevation(latitude, - longitude) - coordinates = dict(lat=latitude, lon=longitude, msl=elevation) diff --git a/homeassistant/components/sun.py b/homeassistant/components/sun.py index 791fec791f8..4b2cd10b781 100644 --- a/homeassistant/components/sun.py +++ b/homeassistant/components/sun.py @@ -12,7 +12,6 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import ( track_point_in_utc_time, track_utc_time_change) from homeassistant.util import dt as dt_util -from homeassistant.util import location as location_util from homeassistant.const import CONF_ELEVATION REQUIREMENTS = ['astral==1.2'] @@ -108,7 +107,7 @@ def setup(hass, config): elevation = platform_config.get(CONF_ELEVATION) if elevation is None: - elevation = location_util.elevation(latitude, longitude) + elevation = hass.config.elevation or 0 from astral import Location diff --git a/homeassistant/components/switch/homematic.py b/homeassistant/components/switch/homematic.py index 16cc63a6708..e9f103b95fa 100644 --- a/homeassistant/components/switch/homematic.py +++ b/homeassistant/components/switch/homematic.py @@ -1,21 +1,9 @@ """ -The homematic switch platform. +Support for Homematic switches. For more details about this platform, please refer to the documentation at 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: # e.g. "JEQ0XXXXXXX" - name: (optional) - button: n (integer of channel to map, device-dependent) (optional) """ - import logging from homeassistant.components.switch import SwitchDevice from homeassistant.const import STATE_UNKNOWN @@ -27,19 +15,17 @@ DEPENDENCIES = ['homematic'] def setup_platform(hass, config, add_callback_devices, discovery_info=None): - """Setup the platform.""" - if discovery_info: - return homematic.setup_hmdevice_discovery_helper(HMSwitch, - discovery_info, - add_callback_devices) - # Manual - return homematic.setup_hmdevice_entity_helper(HMSwitch, - config, - add_callback_devices) + """Setup the Homematic switch platform.""" + if discovery_info is None: + return + + return homematic.setup_hmdevice_discovery_helper(HMSwitch, + discovery_info, + add_callback_devices) class HMSwitch(homematic.HMDevice, SwitchDevice): - """Represents a Homematic Switch in Home Assistant.""" + """Representation of a Homematic switch.""" @property def is_on(self): @@ -71,24 +57,24 @@ class HMSwitch(homematic.HMDevice, SwitchDevice): self._hmdevice.off(self._channel) 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 # Check compatibility from HMDevice if not super()._check_hm_to_ha_object(): 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): return True if isinstance(self._hmdevice, Dimmer): 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 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, SwitchPowermeter diff --git a/homeassistant/components/switch/thinkingcleaner.py b/homeassistant/components/switch/thinkingcleaner.py index 3bc4484db38..46adc5a7052 100644 --- a/homeassistant/components/switch/thinkingcleaner.py +++ b/homeassistant/components/switch/thinkingcleaner.py @@ -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 logging from datetime import timedelta diff --git a/homeassistant/components/switch/wake_on_lan.py b/homeassistant/components/switch/wake_on_lan.py index 28a44249e12..779f4759442 100644 --- a/homeassistant/components/switch/wake_on_lan.py +++ b/homeassistant/components/switch/wake_on_lan.py @@ -52,7 +52,7 @@ class WOLSwitch(SwitchDevice): @property def is_on(self): - """True if switch is on.""" + """Return true if switch is on.""" return self._state @property diff --git a/homeassistant/components/switch/wink.py b/homeassistant/components/switch/wink.py index a5b67f5ddcf..64c19e34bc9 100644 --- a/homeassistant/components/switch/wink.py +++ b/homeassistant/components/switch/wink.py @@ -6,10 +6,11 @@ https://home-assistant.io/components/switch.wink/ """ import logging -from homeassistant.components.wink import WinkToggleDevice +from homeassistant.components.wink import WinkDevice 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): @@ -31,3 +32,24 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(WinkToggleDevice(switch) for switch in pywink.get_powerstrip_outlets()) 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) diff --git a/homeassistant/components/thermostat/eq3btsmart.py b/homeassistant/components/thermostat/eq3btsmart.py index c9bbdaeb0a4..17f166a297e 100644 --- a/homeassistant/components/thermostat/eq3btsmart.py +++ b/homeassistant/components/thermostat/eq3btsmart.py @@ -1,16 +1,16 @@ """ 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 from homeassistant.components.thermostat import ThermostatDevice from homeassistant.const import TEMP_CELCIUS from homeassistant.helpers.temperature import convert -REQUIREMENTS = ['bluepy_devices>=0.2.0'] +REQUIREMENTS = ['bluepy_devices==0.2.0'] CONF_MAC = 'mac' CONF_DEVICES = 'devices' diff --git a/homeassistant/components/thermostat/homematic.py b/homeassistant/components/thermostat/homematic.py index d7675a5cd47..345b8785b42 100644 --- a/homeassistant/components/thermostat/homematic.py +++ b/homeassistant/components/thermostat/homematic.py @@ -1,20 +1,9 @@ """ -The Homematic thermostat platform. +Support for Homematic thermostats. For more details about this platform, please refer to the documentation at 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: "" # e.g. "JEQ0XXXXXXX" - name: "" (optional) """ - import logging import homeassistant.components.homematic as homematic 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): - """Setup the platform.""" - if discovery_info: - return homematic.setup_hmdevice_discovery_helper(HMThermostat, - discovery_info, - add_callback_devices) - # Manual - return homematic.setup_hmdevice_entity_helper(HMThermostat, - config, - add_callback_devices) + """Setup the Homematic thermostat platform.""" + if discovery_info is None: + return + + return homematic.setup_hmdevice_discovery_helper(HMThermostat, + discovery_info, + add_callback_devices) # pylint: disable=abstract-method class HMThermostat(homematic.HMDevice, ThermostatDevice): - """Represents a Homematic Thermostat in Home Assistant.""" + """Representation of a Homematic thermostat.""" @property def unit_of_measurement(self): @@ -78,7 +65,7 @@ class HMThermostat(homematic.HMDevice, ThermostatDevice): return convert(30.5, TEMP_CELSIUS, self.unit_of_measurement) 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\ as pyHMThermostat @@ -86,7 +73,7 @@ class HMThermostat(homematic.HMDevice, ThermostatDevice): if not super()._check_hm_to_ha_object(): 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): return True @@ -94,7 +81,7 @@ class HMThermostat(homematic.HMDevice, ThermostatDevice): return False 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() # Add state to data dict diff --git a/homeassistant/components/vera.py b/homeassistant/components/vera.py index ee55ec858cc..455927ca999 100644 --- a/homeassistant/components/vera.py +++ b/homeassistant/components/vera.py @@ -13,12 +13,13 @@ from homeassistant.helpers import discovery from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['pyvera==0.2.10'] +REQUIREMENTS = ['pyvera==0.2.12'] _LOGGER = logging.getLogger(__name__) DOMAIN = 'vera' + VERA_CONTROLLER = None CONF_EXCLUDE = 'exclude' @@ -33,6 +34,7 @@ DEVICE_CATEGORIES = { 'Switch': 'switch', 'Armable Sensor': 'switch', 'On/Off Switch': 'switch', + 'Doorlock': 'lock', # 'Window Covering': NOT SUPPORTED YET } @@ -91,7 +93,7 @@ def setup(hass, base_config): dev_type = 'light' 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) return True diff --git a/homeassistant/components/wink.py b/homeassistant/components/wink.py index 85bc7f46cef..4e9fec77ba5 100644 --- a/homeassistant/components/wink.py +++ b/homeassistant/components/wink.py @@ -5,13 +5,17 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/wink/ """ import logging +import json -from homeassistant.const import CONF_ACCESS_TOKEN, ATTR_BATTERY_LEVEL 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" -REQUIREMENTS = ['python-wink==0.7.7'] +REQUIREMENTS = ['python-wink==0.7.8', 'pubnub==3.7.8'] + +SUBSCRIPTION_HANDLER = None +CHANNELS = [] def setup(hass, config): @@ -22,7 +26,11 @@ def setup(hass, config): return False import pywink + from pubnub import Pubnub 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 for component_name, func_exists in ( @@ -41,13 +49,33 @@ def setup(hass, config): return True -class WinkToggleDevice(ToggleEntity): - """Represents a Wink toggle (switch) device.""" +class WinkDevice(Entity): + """Represents a base Wink device.""" def __init__(self, wink): """Initialize the Wink device.""" + from pubnub import Pubnub self.wink = wink 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 def unique_id(self): @@ -59,28 +87,20 @@ class WinkToggleDevice(ToggleEntity): """Return the name of the device.""" return self.wink.name() - @property - def is_on(self): - """Return true if device is on.""" - return self.wink.state() - @property def available(self): """True if connection == True.""" 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): """Update state of the device.""" 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 def device_state_attributes(self): """Return the state attributes.""" diff --git a/homeassistant/components/zwave.py b/homeassistant/components/zwave.py index 84ec3dfd847..f8959f33033 100644 --- a/homeassistant/components/zwave.py +++ b/homeassistant/components/zwave.py @@ -41,6 +41,7 @@ EVENT_SCENE_ACTIVATED = "zwave.scene_activated" COMMAND_CLASS_WHATEVER = None COMMAND_CLASS_SENSOR_MULTILEVEL = 49 +COMMAND_CLASS_COLOR = 51 COMMAND_CLASS_METER = 50 COMMAND_CLASS_ALARM = 113 COMMAND_CLASS_SWITCH_BINARY = 37 diff --git a/homeassistant/config.py b/homeassistant/config.py index e8981e520c8..55e97f67c7e 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -1,31 +1,35 @@ """Module to help with parsing and generating configuration files.""" import logging import os +import shutil from types import MappingProxyType import voluptuous as vol -import homeassistant.util.location as loc_util from homeassistant.const import ( 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.util.yaml import load_yaml 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__) YAML_CONFIG_FILE = 'configuration.yaml' +VERSION_FILE = '.HA_VERSION' CONFIG_DIR_NAME = '.homeassistant' DEFAULT_CONFIG = ( # Tuples (attribute, default, auto detect property, description) (CONF_NAME, 'Home', None, 'Name of the location where Home Assistant is ' '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'), - (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_TIME_ZONE, 'UTC', 'time_zone', 'Pick yours from here: http://en.wiki' 'pedia.org/wiki/List_of_tz_database_time_zones'), @@ -39,7 +43,7 @@ DEFAULT_COMPONENTS = { 'history:': 'Enables support for tracking state changes over time.', 'logbook:': 'View all events in a logbook', '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_LATITUDE: cv.latitude, CONF_LONGITUDE: cv.longitude, + CONF_ELEVATION: vol.Coerce(float), CONF_TEMPERATURE_UNIT: cv.temperature_unit, CONF_TIME_ZONE: cv.time_zone, 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. """ 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} @@ -111,6 +117,10 @@ def create_default_config(config_dir, detect_location=True): continue 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 # So we're hard coding a YAML template. try: @@ -130,6 +140,9 @@ def create_default_config(config_dir, detect_location=True): config_file.write("# {}\n".format(description)) config_file.write("{}\n\n".format(component)) + with open(version_path, 'wt') as version_file: + version_file.write(__version__) + return config_path except IOError: @@ -155,3 +168,112 @@ def load_yaml_config_file(config_path): raise HomeAssistantError(msg) 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)) diff --git a/homeassistant/core.py b/homeassistant/core.py index d3eed6ce5e0..82ec20c82f9 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -49,6 +49,19 @@ MIN_WORKER_THREAD = 2 _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): """Root object of the Home Assistant home automation.""" @@ -59,14 +72,23 @@ class HomeAssistant(object): self.services = ServiceRegistry(self.bus, pool) self.states = StateMachine(self.bus) 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): """Start home assistant.""" _LOGGER.info( "Starting Home Assistant (%d threads)", self.pool.worker_count) + self.state = CoreState.starting create_timer(self) self.bus.fire(EVENT_HOMEASSISTANT_START) + self.pool.block_till_done() + self.state = CoreState.running def block_till_stopped(self): """Register service homeassistant/stop and will block until called.""" @@ -113,8 +135,10 @@ class HomeAssistant(object): def stop(self): """Stop Home Assistant and shuts down all threads.""" _LOGGER.info("Stopping") + self.state = CoreState.stopping self.bus.fire(EVENT_HOMEASSISTANT_STOP) self.pool.stop() + self.state = CoreState.not_running class JobPriority(util.OrderedEnum): @@ -681,6 +705,7 @@ class Config(object): """Initialize a new config object.""" self.latitude = None self.longitude = None + self.elevation = None self.temperature_unit = None self.location_name = None self.time_zone = None diff --git a/homeassistant/remote.py b/homeassistant/remote.py index b2dfc3ae18f..6c49decdff2 100644 --- a/homeassistant/remote.py +++ b/homeassistant/remote.py @@ -11,6 +11,7 @@ from datetime import datetime import enum import json import logging +import time import threading import urllib.parse @@ -123,6 +124,7 @@ class HomeAssistant(ha.HomeAssistant): self.services = ha.ServiceRegistry(self.bus, pool) self.states = StateMachine(self.bus, self.remote_api) self.config = ha.Config() + self.state = ha.CoreState.not_running self.config.api = local_api @@ -134,17 +136,20 @@ class HomeAssistant(ha.HomeAssistant): raise HomeAssistantError( 'Unable to setup local API to receive events') + self.state = ha.CoreState.starting ha.create_timer(self) self.bus.fire(ha.EVENT_HOMEASSISTANT_START, origin=ha.EventOrigin.remote) - # Give eventlet time to startup - import eventlet - eventlet.sleep(0.1) + # Ensure local HTTP is started + self.pool.block_till_done() + self.state = ha.CoreState.running + time.sleep(0.05) # 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): raise HomeAssistantError(( 'Could not setup event forwarding from api {} to ' @@ -153,6 +158,7 @@ class HomeAssistant(ha.HomeAssistant): def stop(self): """Stop Home Assistant and shuts down all threads.""" _LOGGER.info("Stopping") + self.state = ha.CoreState.stopping self.bus.fire(ha.EVENT_HOMEASSISTANT_STOP, origin=ha.EventOrigin.remote) @@ -161,6 +167,7 @@ class HomeAssistant(ha.HomeAssistant): # Disconnect master event forwarding disconnect_remote_events(self.remote_api, self.config.api) + self.state = ha.CoreState.not_running class EventBus(ha.EventBus): diff --git a/homeassistant/util/location.py b/homeassistant/util/location.py index a596d9bc476..a9b980bc871 100644 --- a/homeassistant/util/location.py +++ b/homeassistant/util/location.py @@ -8,7 +8,8 @@ import math import requests 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 # Earth ellipsoid according to WGS 84 @@ -32,30 +33,13 @@ LocationInfo = collections.namedtuple( def detect_location_info(): """Detect location information.""" - success = None + data = _get_freegeoip() - for source in DATA_SOURCE: - try: - raw_info = requests.get(source, timeout=5).json() - success = source - break - except (requests.RequestException, ValueError): - success = False + if data is None: + data = _get_ip_api() - if success is False: + if data is 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, # the Cayman Islands, Palau, and the United States and associated @@ -73,11 +57,16 @@ def distance(lat1, lon1, lat2, lon2): def elevation(latitude, longitude): """Return elevation for given latitude and longitude.""" - req = requests.get(ELEVATION_URL, - params={'locations': '{},{}'.format(latitude, - longitude), - 'sensor': 'false'}, - timeout=10) + try: + req = requests.get( + ELEVATION_URL, + params={ + 'locations': '{},{}'.format(latitude, longitude), + 'sensor': 'false', + }, + timeout=10) + except requests.RequestException: + return 0 if req.status_code != 200: return 0 @@ -157,3 +146,45 @@ def vincenty(point1, point2, miles=False): s *= MILES_PER_KILOMETER # kilometers to miles 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'), + } diff --git a/requirements_all.txt b/requirements_all.txt index a131813edbd..2dc4da44710 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -5,7 +5,6 @@ pytz>=2016.4 pip>=7.0.0 jinja2>=2.8 voluptuous==0.8.9 -eventlet==0.19.0 # homeassistant.components.isy994 PyISY==1.0.6 @@ -23,7 +22,7 @@ SoCo==0.11.1 TwitterAPI==2.4.1 # homeassistant.components.http -Werkzeug==0.11.5 +Werkzeug==0.11.10 # homeassistant.components.apcupsd apcaccess==0.0.4 @@ -41,13 +40,16 @@ blinkstick==1.1.7 blockchain==1.3.3 # homeassistant.components.thermostat.eq3btsmart -# bluepy_devices>=0.2.0 +# bluepy_devices==0.2.0 # homeassistant.components.notify.aws_lambda # homeassistant.components.notify.aws_sns # homeassistant.components.notify.aws_sqs boto3==1.3.1 +# homeassistant.components.http +cherrypy==6.0.2 + # homeassistant.components.notify.xmpp dnspython3==1.12.0 @@ -61,9 +63,6 @@ eliqonline==1.0.12 # homeassistant.components.enocean enocean==0.31 -# homeassistant.components.http -eventlet==0.19.0 - # homeassistant.components.thermostat.honeywell 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 # homeassistant.components.influxdb -influxdb==2.12.0 +influxdb==3.0.0 # homeassistant.components.insteon_hub insteon_hub==0.4.5 # homeassistant.components.media_player.kodi -jsonrpc-requests==0.2 +jsonrpc-requests==0.3 # homeassistant.components.light.lifx liffylights==0.9.4 @@ -220,6 +219,16 @@ proliphix==0.1.0 # homeassistant.components.sensor.systemmonitor 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 pushbullet.py==0.10.0 @@ -245,7 +254,7 @@ pyasn1==0.1.9 pychromecast==0.7.2 # homeassistant.components.media_player.cmus -pycmus>=0.1.0 +pycmus==0.1.0 # homeassistant.components.envisalink # homeassistant.components.zwave @@ -258,7 +267,7 @@ pyenvisalink==1.0 pyfttt==0.3 # homeassistant.components.homematic -pyhomematic==0.1.6 +pyhomematic==0.1.8 # homeassistant.components.device_tracker.icloud pyicloud==0.8.3 @@ -324,13 +333,13 @@ python-twitch==1.2.0 # homeassistant.components.rollershutter.wink # homeassistant.components.sensor.wink # homeassistant.components.switch.wink -python-wink==0.7.7 +python-wink==0.7.8 # homeassistant.components.keyboard pyuserinput==0.1.9 # homeassistant.components.vera -pyvera==0.2.10 +pyvera==0.2.12 # homeassistant.components.wemo pywemo==0.4.3 @@ -410,7 +419,7 @@ vsure==0.8.1 wakeonlan==0.2.2 # homeassistant.components.media_player.gpmdp -websocket-client==0.35.0 +websocket-client==0.37.0 # homeassistant.components.zigbee xbee-helper==0.0.7 diff --git a/requirements_test.txt b/requirements_test.txt index 5ec8619b37f..649859f2506 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,10 +1,9 @@ -flake8>=2.5.4 -pylint>=1.5.5 +flake8>=2.6.0 +pylint>=1.5.6 coveralls>=1.1 -pytest>=2.9.1 -pytest-cov>=2.2.0 +pytest>=2.9.2 +pytest-cov>=2.2.1 pytest-timeout>=1.0.0 pytest-capturelog>=0.7 -betamax==0.7.0 pydocstyle>=1.0.0 -httpretty==0.8.14 +requests_mock>=1.0 diff --git a/setup.py b/setup.py index b574e156931..fbce912c3d6 100755 --- a/setup.py +++ b/setup.py @@ -17,7 +17,6 @@ REQUIRES = [ 'pip>=7.0.0', 'jinja2>=2.8', 'voluptuous==0.8.9', - 'eventlet==0.19.0', ] setup( diff --git a/tests/__init__.py b/tests/__init__.py index c1f50d86dfb..a931604fdce 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,27 +1,25 @@ -"""Test the initialization.""" -import betamax +"""Setup some common test helper things.""" +import functools from homeassistant import util 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 -# so mocked by default. -location.detect_location_info = lambda: location.LocationInfo( - ip='1.1.1.1', - 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='2.0', - longitude='1.0', - use_fahrenheit=True, -) +def test_real(func): + """Force a function to require a keyword _test_real to be passed in.""" + @functools.wraps(func) + def guard_func(*args, **kwargs): + real = kwargs.pop('_test_real', None) -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' diff --git a/tests/cassettes/tests.components.sensor.test_yr.TestSensorYr.test_custom_setup.json b/tests/cassettes/tests.components.sensor.test_yr.TestSensorYr.test_custom_setup.json deleted file mode 100644 index c647c4ae017..00000000000 --- a/tests/cassettes/tests.components.sensor.test_yr.TestSensorYr.test_custom_setup.json +++ /dev/null @@ -1 +0,0 @@ -{"http_interactions": [{"request": {"uri": "http://api.yr.no/weatherapi/locationforecast/1.9/?lat=32.87336;lon=117.22743;msl=0", "method": "GET", "headers": {"Accept": ["*/*"], "User-Agent": ["python-requests/2.9.1"], "Accept-Encoding": ["gzip, deflate"], "Connection": ["keep-alive"]}, "body": {"encoding": "utf-8", "string": ""}}, "recorded_at": "2016-06-09T04:02:23", "response": {"url": "http://api.yr.no/weatherapi/locationforecast/1.9/?lat=32.87336;lon=117.22743;msl=0", "headers": {"Location": ["http://api.met.no/weatherapi/locationforecast/1.9/?lat=32.87336;lon=117.22743;msl=0"], "Age": ["0"], "X-Varnish": ["4249781791"], "Via": ["1.1 varnish"], "Server": ["Varnish"], "Date": ["Thu, 09 Jun 2016 04:02:21 GMT"], "Connection": ["close"], "Accept-Ranges": ["bytes"]}, "body": {"encoding": null, "string": ""}, "status": {"message": "Moved permanently", "code": 301}}}, {"request": {"uri": "http://api.met.no/weatherapi/locationforecast/1.9/?lat=32.87336;lon=117.22743;msl=0", "method": "GET", "headers": {"Accept": ["*/*"], "User-Agent": ["python-requests/2.9.1"], "Accept-Encoding": ["gzip, deflate"], "Connection": ["keep-alive"]}, "body": {"encoding": "utf-8", "string": ""}}, "recorded_at": "2016-06-09T04:02:23", "response": {"url": "http://api.met.no/weatherapi/locationforecast/1.9/?lat=32.87336;lon=117.22743;msl=0", "headers": {"Content-Length": ["3637"], "X-Backend-Host": ["ravn_loc"], "X-Varnish": ["4249782320 4249780502"], "Via": ["1.1 varnish"], "Last-Modified": ["Thu, 09 Jun 2016 04:02:21 GMT"], "X-forecast-models": ["proff,ecdet"], "Accept-Ranges": ["bytes"], "Expires": ["Thu, 09 Jun 2016 05:01:15 GMT"], "Content-Encoding": ["gzip"], "Age": ["1"], "Content-Type": ["text/xml; charset=utf-8"], "Server": ["Apache"], "Vary": ["Accept-Encoding"], "Date": ["Thu, 09 Jun 2016 04:02:22 GMT"], "X-slicenumber": ["83"], "Connection": ["keep-alive"]}, "body": {"encoding": "utf-8", "base64_string": "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"}, "status": {"message": "OK", "code": 200}}}], "recorded_with": "betamax/0.7.0"} \ No newline at end of file diff --git a/tests/cassettes/tests.components.sensor.test_yr.TestSensorYr.test_default_setup.json b/tests/cassettes/tests.components.sensor.test_yr.TestSensorYr.test_default_setup.json deleted file mode 100644 index 8226cbbf96e..00000000000 --- a/tests/cassettes/tests.components.sensor.test_yr.TestSensorYr.test_default_setup.json +++ /dev/null @@ -1 +0,0 @@ -{"http_interactions": [{"request": {"uri": "http://api.yr.no/weatherapi/locationforecast/1.9/?lat=32.87336;lon=117.22743;msl=0", "method": "GET", "headers": {"Accept": ["*/*"], "User-Agent": ["python-requests/2.9.1"], "Accept-Encoding": ["gzip, deflate"], "Connection": ["keep-alive"]}, "body": {"encoding": "utf-8", "string": ""}}, "recorded_at": "2016-06-09T04:02:22", "response": {"url": "http://api.yr.no/weatherapi/locationforecast/1.9/?lat=32.87336;lon=117.22743;msl=0", "headers": {"Location": ["http://api.met.no/weatherapi/locationforecast/1.9/?lat=32.87336;lon=117.22743;msl=0"], "Age": ["0"], "X-Varnish": ["4249779869"], "Via": ["1.1 varnish"], "Server": ["Varnish"], "Date": ["Thu, 09 Jun 2016 04:02:20 GMT"], "Connection": ["close"], "Accept-Ranges": ["bytes"]}, "body": {"encoding": null, "string": ""}, "status": {"message": "Moved permanently", "code": 301}}}, {"request": {"uri": "http://api.met.no/weatherapi/locationforecast/1.9/?lat=32.87336;lon=117.22743;msl=0", "method": "GET", "headers": {"Accept": ["*/*"], "User-Agent": ["python-requests/2.9.1"], "Accept-Encoding": ["gzip, deflate"], "Connection": ["keep-alive"]}, "body": {"encoding": "utf-8", "string": ""}}, "recorded_at": "2016-06-09T04:02:22", "response": {"url": "http://api.met.no/weatherapi/locationforecast/1.9/?lat=32.87336;lon=117.22743;msl=0", "headers": {"Content-Length": ["3637"], "X-Backend-Host": ["ravn_loc"], "X-Varnish": ["4249780502"], "Via": ["1.1 varnish"], "Last-Modified": ["Thu, 09 Jun 2016 04:02:21 GMT"], "X-forecast-models": ["proff,ecdet"], "Accept-Ranges": ["bytes"], "Expires": ["Thu, 09 Jun 2016 05:01:15 GMT"], "Content-Encoding": ["gzip"], "Age": ["0"], "Content-Type": ["text/xml; charset=utf-8"], "Server": ["Apache"], "Vary": ["Accept-Encoding"], "Date": ["Thu, 09 Jun 2016 04:02:21 GMT"], "X-slicenumber": ["83"], "Connection": ["keep-alive"]}, "body": {"encoding": "utf-8", "base64_string": "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"}, "status": {"message": "OK", "code": 200}}}], "recorded_with": "betamax/0.7.0"} \ No newline at end of file diff --git a/tests/common.py b/tests/common.py index 98c61dfc16e..26d466bc4b8 100644 --- a/tests/common.py +++ b/tests/common.py @@ -35,6 +35,7 @@ def get_test_home_assistant(num_threads=None): hass.config.config_dir = get_test_config_dir() hass.config.latitude = 32.87336 hass.config.longitude = -117.22743 + hass.config.elevation = 0 hass.config.time_zone = date_util.get_time_zone('US/Pacific') 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)) +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): """Mock state change envent.""" event_data = { diff --git a/tests/components/device_tracker/test_locative.py b/tests/components/device_tracker/test_locative.py index 7445b5daf8c..427980be5f1 100644 --- a/tests/components/device_tracker/test_locative.py +++ b/tests/components/device_tracker/test_locative.py @@ -1,8 +1,8 @@ """The tests the for Locative device tracker platform.""" +import time import unittest from unittest.mock import patch -import eventlet import requests from homeassistant import bootstrap, const @@ -32,12 +32,9 @@ def setUpModule(): # pylint: disable=invalid-name bootstrap.setup_component(hass, http.DOMAIN, { http.DOMAIN: { http.CONF_SERVER_PORT: SERVER_PORT - } + }, }) - # Set up API - bootstrap.setup_component(hass, 'api') - # Set up device tracker bootstrap.setup_component(hass, device_tracker.DOMAIN, { device_tracker.DOMAIN: { @@ -46,7 +43,7 @@ def setUpModule(): # pylint: disable=invalid-name }) hass.start() - eventlet.sleep(0.05) + time.sleep(0.05) def tearDownModule(): # pylint: disable=invalid-name diff --git a/tests/components/test_forecast.py b/tests/components/sensor/test_forecast.py similarity index 68% rename from tests/components/test_forecast.py rename to tests/components/sensor/test_forecast.py index bfda22596c2..55bdec20a35 100644 --- a/tests/components/test_forecast.py +++ b/tests/components/sensor/test_forecast.py @@ -1,17 +1,17 @@ """The tests for the forecast.io platform.""" -import json import re -import os import unittest from unittest.mock import MagicMock, patch import forecastio -import httpretty from requests.exceptions import HTTPError +import requests_mock from homeassistant.components.sensor import forecast from homeassistant import core as ha +from tests.common import load_fixture + class TestForecastSetup(unittest.TestCase): """Test the forecast.io platform.""" @@ -48,29 +48,14 @@ class TestForecastSetup(unittest.TestCase): response = forecast.setup_platform(self.hass, self.config, MagicMock()) self.assertFalse(response) - @httpretty.activate + @requests_mock.Mocker() @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.""" - def load_fixture_from_json(): - cwd = os.path.dirname(__file__) - fixture_path = os.path.join(cwd, '..', 'fixtures', 'forecast.json') - with open(fixture_path) as file: - 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 - + uri = ('https://api.forecast.io\/forecast\/(\w+)\/' + '(-?\d+\.?\d*),(-?\d+\.?\d*)') + m.get(re.compile(uri), + text=load_fixture('forecast.json')) forecast.setup_platform(self.hass, self.config, MagicMock()) self.assertTrue(mock_get_forecast.called) self.assertEqual(mock_get_forecast.call_count, 1) diff --git a/tests/components/sensor/test_yr.py b/tests/components/sensor/test_yr.py index 43a14578690..3ea94938f0d 100644 --- a/tests/components/sensor/test_yr.py +++ b/tests/components/sensor/test_yr.py @@ -1,39 +1,40 @@ """The tests for the Yr sensor platform.""" from datetime import datetime +from unittest import TestCase from unittest.mock import patch -import pytest +import requests_mock from homeassistant.bootstrap import _setup_component 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: +class TestSensorYr(TestCase): """Test the Yr sensor.""" - def setup_method(self, method): + def setUp(self): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() self.hass.config.latitude = 32.87336 self.hass.config.longitude = 117.22743 - def teardown_method(self, method): + def tearDown(self): """Stop everything that was started.""" self.hass.stop() - def test_default_setup(self, betamax_session): + @requests_mock.Mocker() + def test_default_setup(self, m): """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) - with patch('homeassistant.components.sensor.yr.requests.Session', - return_value=betamax_session): - with patch('homeassistant.components.sensor.yr.dt_util.utcnow', - return_value=now): - assert _setup_component(self.hass, 'sensor', { - 'sensor': {'platform': 'yr', - 'elevation': 0}}) + with patch('homeassistant.components.sensor.yr.dt_util.utcnow', + return_value=now): + assert _setup_component(self.hass, 'sensor', { + 'sensor': {'platform': 'yr', + 'elevation': 0}}) state = self.hass.states.get('sensor.yr_symbol') @@ -41,23 +42,24 @@ class TestSensorYr: assert state.state.isnumeric() 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.""" + 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) - with patch('homeassistant.components.sensor.yr.requests.Session', - return_value=betamax_session): - with patch('homeassistant.components.sensor.yr.dt_util.utcnow', - return_value=now): - assert _setup_component(self.hass, 'sensor', { - 'sensor': {'platform': 'yr', - 'elevation': 0, - 'monitored_conditions': [ - 'pressure', - 'windDirection', - 'humidity', - 'fog', - 'windSpeed']}}) + with patch('homeassistant.components.sensor.yr.dt_util.utcnow', + return_value=now): + assert _setup_component(self.hass, 'sensor', { + 'sensor': {'platform': 'yr', + 'elevation': 0, + 'monitored_conditions': [ + 'pressure', + 'windDirection', + 'humidity', + 'fog', + 'windSpeed']}}) state = self.hass.states.get('sensor.yr_pressure') assert 'hPa' == state.attributes.get('unit_of_measurement') diff --git a/tests/components/test_alexa.py b/tests/components/test_alexa.py index e1eb257577c..97d73b8b49d 100644 --- a/tests/components/test_alexa.py +++ b/tests/components/test_alexa.py @@ -1,9 +1,9 @@ """The tests for the Alexa component.""" # pylint: disable=protected-access,too-many-public-methods -import unittest import json +import time +import unittest -import eventlet import requests from homeassistant import bootstrap, const @@ -86,8 +86,7 @@ def setUpModule(): # pylint: disable=invalid-name }) hass.start() - - eventlet.sleep(0.1) + time.sleep(0.05) def tearDownModule(): # pylint: disable=invalid-name diff --git a/tests/components/test_api.py b/tests/components/test_api.py index 8d1ee1c4ad5..752980e65c8 100644 --- a/tests/components/test_api.py +++ b/tests/components/test_api.py @@ -1,12 +1,12 @@ """The tests for the Home Assistant API component.""" # pylint: disable=protected-access,too-many-public-methods -# from contextlib import closing +from contextlib import closing import json import tempfile +import time import unittest from unittest.mock import patch -import eventlet import requests from homeassistant import bootstrap, const @@ -48,10 +48,7 @@ def setUpModule(): # pylint: disable=invalid-name bootstrap.setup_component(hass, 'api') hass.start() - - # To start HTTP - # TODO fix this - eventlet.sleep(0.05) + time.sleep(0.05) def tearDownModule(): # pylint: disable=invalid-name @@ -387,25 +384,23 @@ class TestAPI(unittest.TestCase): headers=HA_HEADERS) self.assertEqual(422, req.status_code) - # TODO disabled because eventlet cannot validate - # a connection to itself, need a second instance - # # Setup a real one - # req = requests.post( - # _url(const.URL_API_EVENT_FORWARD), - # data=json.dumps({ - # 'api_password': API_PASSWORD, - # 'host': '127.0.0.1', - # 'port': SERVER_PORT - # }), - # headers=HA_HEADERS) - # self.assertEqual(200, req.status_code) + # Setup a real one + req = requests.post( + _url(const.URL_API_EVENT_FORWARD), + data=json.dumps({ + 'api_password': API_PASSWORD, + 'host': '127.0.0.1', + 'port': SERVER_PORT + }), + headers=HA_HEADERS) + self.assertEqual(200, req.status_code) - # # Delete it again.. - # req = requests.delete( - # _url(const.URL_API_EVENT_FORWARD), - # data=json.dumps({}), - # headers=HA_HEADERS) - # self.assertEqual(400, req.status_code) + # Delete it again.. + req = requests.delete( + _url(const.URL_API_EVENT_FORWARD), + data=json.dumps({}), + headers=HA_HEADERS) + self.assertEqual(400, req.status_code) req = requests.delete( _url(const.URL_API_EVENT_FORWARD), @@ -425,57 +420,58 @@ class TestAPI(unittest.TestCase): headers=HA_HEADERS) self.assertEqual(200, req.status_code) - # def test_stream(self): - # """Test the stream.""" - # listen_count = self._listen_count() - # with closing(requests.get(_url(const.URL_API_STREAM), timeout=3, - # stream=True, headers=HA_HEADERS)) as req: + def test_stream(self): + """Test the stream.""" + listen_count = self._listen_count() + with closing(requests.get(_url(const.URL_API_STREAM), timeout=3, + 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): - # """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: - # self.assertEqual(listen_count + 1, self._listen_count()) + hass.bus.fire('test_event1') + data = self._stream_next_event(stream) + self.assertEqual('test_event1', data['event_type']) - # hass.bus.fire('test_event1') - # data = self._stream_next_event(req) - # self.assertEqual('test_event1', data['event_type']) + hass.bus.fire('test_event2') + hass.bus.fire('test_event3') - # hass.bus.fire('test_event2') - # hass.bus.fire('test_event3') + data = self._stream_next_event(stream) + self.assertEqual('test_event3', data['event_type']) - # data = self._stream_next_event(req) - # self.assertEqual('test_event3', data['event_type']) + def _stream_next_event(self, stream): + """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): - # """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:] - # conv = data.decode('utf-8').strip()[6:] + if conv != 'ping': + break - # if conv != 'ping': - # break + return json.loads(conv) - # return json.loads(conv) - - # def _listen_count(self): - # """Return number of event listeners.""" - # return sum(hass.bus.listeners.values()) + def _listen_count(self): + """Return number of event listeners.""" + return sum(hass.bus.listeners.values()) diff --git a/tests/components/test_frontend.py b/tests/components/test_frontend.py index 61e33931c24..083ebd2eb0c 100644 --- a/tests/components/test_frontend.py +++ b/tests/components/test_frontend.py @@ -1,9 +1,9 @@ """The tests for Home Assistant frontend.""" # pylint: disable=protected-access,too-many-public-methods import re +import time import unittest -import eventlet import requests import homeassistant.bootstrap as bootstrap @@ -42,10 +42,7 @@ def setUpModule(): # pylint: disable=invalid-name bootstrap.setup_component(hass, 'frontend') hass.start() - - # Give eventlet time to start - # TODO fix this - eventlet.sleep(0.05) + time.sleep(0.05) def tearDownModule(): # pylint: disable=invalid-name diff --git a/tests/components/test_http.py b/tests/components/test_http.py index f665a9530c8..6ab79f3e0cc 100644 --- a/tests/components/test_http.py +++ b/tests/components/test_http.py @@ -1,8 +1,8 @@ """The tests for the Home Assistant HTTP component.""" # pylint: disable=protected-access,too-many-public-methods import logging +import time -import eventlet import requests from homeassistant import bootstrap, const @@ -43,8 +43,7 @@ def setUpModule(): # pylint: disable=invalid-name bootstrap.setup_component(hass, 'api') hass.start() - - eventlet.sleep(0.05) + time.sleep(0.05) def tearDownModule(): # pylint: disable=invalid-name @@ -83,7 +82,7 @@ class TestHttp: logs = caplog.text() - assert const.URL_API in logs + # assert const.URL_API in logs assert API_PASSWORD not in logs def test_access_denied_with_wrong_password_in_url(self): @@ -106,5 +105,5 @@ class TestHttp: logs = caplog.text() - assert const.URL_API in logs + # assert const.URL_API in logs assert API_PASSWORD not in logs diff --git a/tests/components/test_init.py b/tests/components/test_init.py index 68b0ca3be35..7abaf63b407 100644 --- a/tests/components/test_init.py +++ b/tests/components/test_init.py @@ -131,7 +131,7 @@ class TestComponentsCore(unittest.TestCase): assert state.attributes.get('hello') == 'world' @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): """Test reload core conf service.""" with TemporaryDirectory() as conf_dir: diff --git a/tests/fixtures/freegeoip.io.json b/tests/fixtures/freegeoip.io.json new file mode 100644 index 00000000000..8afdaba070e --- /dev/null +++ b/tests/fixtures/freegeoip.io.json @@ -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 +} diff --git a/tests/fixtures/google_maps_elevation.json b/tests/fixtures/google_maps_elevation.json new file mode 100644 index 00000000000..95eeb0fe239 --- /dev/null +++ b/tests/fixtures/google_maps_elevation.json @@ -0,0 +1,13 @@ +{ + "results" : [ + { + "elevation" : 101.5, + "location" : { + "lat" : 32.54321, + "lng" : -117.12345 + }, + "resolution" : 4.8 + } + ], + "status" : "OK" +} diff --git a/tests/fixtures/ip-api.com.json b/tests/fixtures/ip-api.com.json new file mode 100644 index 00000000000..d31d4560589 --- /dev/null +++ b/tests/fixtures/ip-api.com.json @@ -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" +} diff --git a/tests/fixtures/yr.no.json b/tests/fixtures/yr.no.json new file mode 100644 index 00000000000..b181fdfd85b --- /dev/null +++ b/tests/fixtures/yr.no.json @@ -0,0 +1,1184 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index 152818d02e4..34aaa1b83ed 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -1,6 +1,5 @@ """Test the bootstrapping.""" # pylint: disable=too-many-public-methods,protected-access -import os import tempfile from unittest import mock import threading @@ -8,10 +7,7 @@ import threading import voluptuous as vol 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 -from homeassistant.helpers.entity import Entity from homeassistant.helpers.config_validation import PLATFORM_SCHEMA from tests.common import get_test_home_assistant, MockModule, MockPlatform @@ -24,23 +20,22 @@ class TestBootstrap: def setup_method(self, method): """Setup the test.""" + self.backup_cache = loader._COMPONENT_CACHE + if method == self.test_from_config_file: return self.hass = get_test_home_assistant() - self.backup_cache = loader._COMPONENT_CACHE def teardown_method(self, method): """Clean up.""" dt_util.DEFAULT_TIME_ZONE = ORIG_TIMEZONE - - if method == self.test_from_config_file: - return - self.hass.stop() 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.""" components = ['browser', 'conversation', 'script'] with tempfile.NamedTemporaryFile() as fp: @@ -48,71 +43,10 @@ class TestBootstrap: fp.write('{}:\n'.format(comp).encode('utf-8')) fp.flush() - hass = bootstrap.from_config_file(fp.name) + self.hass = bootstrap.from_config_file(fp.name) - components.append('group') - - 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'] + components.append('group') + assert sorted(components) == sorted(self.hass.config.components) def test_handle_setup_circular_dependency(self): """Test the setup of circular dependencies.""" @@ -302,8 +236,7 @@ class TestBootstrap: assert not bootstrap._setup_component(self.hass, 'comp', None) assert 'comp' not in self.hass.config.components - @mock.patch('homeassistant.bootstrap.process_ha_core_config') - def test_home_assistant_core_config_validation(self, mock_process): + def test_home_assistant_core_config_validation(self): """Test if we pass in wrong information for HA conf.""" # Extensive HA conf validation testing is done in test_config.py assert None is bootstrap.from_config_dict({ @@ -311,7 +244,6 @@ class TestBootstrap: 'latitude': 'some string' } }) - assert not mock_process.called def test_component_setup_with_validation_and_dependency(self): """Test all config is passed to dependencies.""" diff --git a/tests/test_config.py b/tests/test_config.py index 8a5ec306b3b..6be3f585967 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,22 +1,28 @@ """Test config utils.""" # pylint: disable=too-many-public-methods,protected-access +import os +import tempfile import unittest import unittest.mock as mock -import os import pytest from voluptuous import MultipleInvalid -from homeassistant.core import DOMAIN, HomeAssistantError +from homeassistant.core import DOMAIN, HomeAssistantError, Config import homeassistant.config as config_util from homeassistant.const import ( 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() YAML_PATH = os.path.join(CONFIG_DIR, config_util.YAML_CONFIG_FILE) +ORIG_TIMEZONE = dt_util.DEFAULT_TIME_ZONE def create_file(path): @@ -30,9 +36,14 @@ class TestConfig(unittest.TestCase): def tearDown(self): # pylint: disable=invalid-name """Clean up.""" + dt_util.DEFAULT_TIME_ZONE = ORIG_TIMEZONE + if os.path.isfile(YAML_PATH): os.remove(YAML_PATH) + if hasattr(self, 'hass'): + self.hass.stop() + def test_create_default_config(self): """Test creation of default config.""" config_util.create_default_config(CONFIG_DIR, False) @@ -108,8 +119,15 @@ class TestConfig(unittest.TestCase): [('hello', 0), ('world', 1)], 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') - 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.""" config_util.ensure_config_exists(CONFIG_DIR) @@ -120,15 +138,16 @@ class TestConfig(unittest.TestCase): ha_conf = config[DOMAIN] expected_values = { - CONF_LATITUDE: 2.0, - CONF_LONGITUDE: 1.0, + CONF_LATITUDE: 32.8594, + CONF_LONGITUDE: -117.2073, + CONF_ELEVATION: 101, CONF_TEMPERATURE_UNIT: 'F', CONF_NAME: 'Home', CONF_TIME_ZONE: 'America/Los_Angeles' } - self.assertEqual(expected_values, ha_conf) - self.assertTrue(mock_print.called) + assert expected_values == ha_conf + assert mock_print.called @mock.patch('builtins.print') 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 diff --git a/tests/test_remote.py b/tests/test_remote.py index 893f02bea31..f3ec35daee5 100644 --- a/tests/test_remote.py +++ b/tests/test_remote.py @@ -1,9 +1,8 @@ """Test Home Assistant remote methods and classes.""" # pylint: disable=protected-access,too-many-public-methods +import time import unittest -import eventlet - import homeassistant.core as ha import homeassistant.bootstrap as bootstrap import homeassistant.remote as remote @@ -47,10 +46,7 @@ def setUpModule(): # pylint: disable=invalid-name bootstrap.setup_component(hass, 'api') hass.start() - - # Give eventlet time to start - # TODO fix this - eventlet.sleep(0.05) + time.sleep(0.05) master_api = remote.API("127.0.0.1", API_PASSWORD, MASTER_PORT) @@ -63,10 +59,6 @@ def setUpModule(): # pylint: disable=invalid-name slave.start() - # Give eventlet time to start - # TODO fix this - eventlet.sleep(0.05) - def tearDownModule(): # pylint: disable=invalid-name """Stop the Home Assistant server and slave.""" @@ -257,7 +249,6 @@ class TestRemoteClasses(unittest.TestCase): slave.pool.block_till_done() # Wait till master gives updated state hass.pool.block_till_done() - eventlet.sleep(0.01) self.assertEqual("remote.statemachine test", slave.states.get("remote.test").state) @@ -266,13 +257,11 @@ class TestRemoteClasses(unittest.TestCase): """Remove statemachine from master.""" hass.states.set("remote.master_remove", "remove me!") hass.pool.block_till_done() - eventlet.sleep(0.01) self.assertIn('remote.master_remove', slave.states.entity_ids()) hass.states.remove("remote.master_remove") hass.pool.block_till_done() - eventlet.sleep(0.01) self.assertNotIn('remote.master_remove', slave.states.entity_ids()) @@ -280,14 +269,12 @@ class TestRemoteClasses(unittest.TestCase): """Remove statemachine from slave.""" hass.states.set("remote.slave_remove", "remove me!") hass.pool.block_till_done() - eventlet.sleep(0.01) self.assertIn('remote.slave_remove', slave.states.entity_ids()) self.assertTrue(slave.states.remove("remote.slave_remove")) slave.pool.block_till_done() hass.pool.block_till_done() - eventlet.sleep(0.01) self.assertNotIn('remote.slave_remove', slave.states.entity_ids()) @@ -306,6 +293,5 @@ class TestRemoteClasses(unittest.TestCase): slave.pool.block_till_done() # Wait till master gives updated event hass.pool.block_till_done() - eventlet.sleep(0.01) self.assertEqual(1, len(test_value)) diff --git a/tests/util/test_location.py b/tests/util/test_location.py index 7d0052fe62c..1dfb71a87bf 100644 --- a/tests/util/test_location.py +++ b/tests/util/test_location.py @@ -1,9 +1,15 @@ """Test Home Assistant location util 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 +from tests.common import load_fixture + # Paris COORDINATES_PARIS = (48.864716, 2.349014) # New York @@ -20,26 +26,124 @@ DISTANCE_KM = 5846.39 DISTANCE_MILES = 3632.78 -class TestLocationUtil(unittest.TestCase): +class TestLocationUtil(TestCase): """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): """Test getting the distance.""" meters = location_util.distance(COORDINATES_PARIS[0], COORDINATES_PARIS[1], COORDINATES_NEW_YORK[0], COORDINATES_NEW_YORK[1]) - self.assertAlmostEqual(meters / 1000, DISTANCE_KM, places=2) + + assert meters/1000 - DISTANCE_KM < 0.01 def test_get_kilometers(self): """Test getting the distance between given coordinates in km.""" kilometers = location_util.vincenty(COORDINATES_PARIS, COORDINATES_NEW_YORK) - self.assertEqual(round(kilometers, 2), DISTANCE_KM) + assert round(kilometers, 2) == DISTANCE_KM def test_get_miles(self): """Test getting the distance between given coordinates in miles.""" miles = location_util.vincenty(COORDINATES_PARIS, COORDINATES_NEW_YORK, 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 diff --git a/tests/util/test_package.py b/tests/util/test_package.py index ea9a8f23dfa..a4e00196959 100644 --- a/tests/util/test_package.py +++ b/tests/util/test_package.py @@ -49,7 +49,7 @@ class TestPackageUtil(unittest.TestCase): self.assertTrue(package.check_package_exists( 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: import pyhelloworld3